first commit
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
package com.bitnix.dirtcryptostore;
|
||||
|
||||
import com.bitnix.dirtcryptostore.command.CryptoStoreCommand;
|
||||
import com.bitnix.dirtcryptostore.listener.StoreGuiListener;
|
||||
import com.bitnix.dirtcryptostore.manager.ConfigManager;
|
||||
import com.bitnix.dirtcryptostore.manager.InvoiceManager;
|
||||
import com.bitnix.dirtcryptostore.manager.PaymentWatcher;
|
||||
import com.bitnix.dirtcryptostore.manager.PricingManager;
|
||||
import com.bitnix.dirtcryptostore.manager.QrMapManager;
|
||||
import org.bukkit.command.PluginCommand;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
import org.bukkit.scheduler.BukkitTask;
|
||||
|
||||
public final class DirtCryptoStorePlugin extends JavaPlugin {
|
||||
|
||||
private static DirtCryptoStorePlugin instance;
|
||||
|
||||
private ConfigManager configManager;
|
||||
private InvoiceManager invoiceManager;
|
||||
private PaymentWatcher paymentWatcher;
|
||||
private QrMapManager qrMapManager;
|
||||
private PricingManager pricingManager;
|
||||
private BukkitTask watcherTask;
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
instance = this;
|
||||
|
||||
saveDefaultConfig();
|
||||
|
||||
this.configManager = new ConfigManager(this);
|
||||
this.configManager.load();
|
||||
|
||||
this.pricingManager = new PricingManager(this);
|
||||
|
||||
this.invoiceManager = new InvoiceManager(this);
|
||||
this.invoiceManager.init();
|
||||
this.invoiceManager.load();
|
||||
|
||||
this.qrMapManager = new QrMapManager(this);
|
||||
|
||||
registerCommands();
|
||||
registerListeners();
|
||||
startWatcher();
|
||||
|
||||
getLogger().info("DirtCryptoStore enabled.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
if (watcherTask != null) {
|
||||
watcherTask.cancel();
|
||||
watcherTask = null;
|
||||
}
|
||||
|
||||
if (invoiceManager != null) {
|
||||
invoiceManager.save();
|
||||
invoiceManager.clearAll();
|
||||
}
|
||||
|
||||
if (qrMapManager != null) {
|
||||
qrMapManager.clearAll();
|
||||
}
|
||||
|
||||
if (pricingManager != null) {
|
||||
pricingManager.clearCache();
|
||||
}
|
||||
|
||||
getLogger().info("DirtCryptoStore disabled.");
|
||||
instance = null;
|
||||
}
|
||||
|
||||
private void registerCommands() {
|
||||
PluginCommand command = getCommand("cryptostore");
|
||||
if (command == null) {
|
||||
getLogger().severe("Command 'cryptostore' is missing from plugin.yml");
|
||||
return;
|
||||
}
|
||||
|
||||
CryptoStoreCommand cryptoStoreCommand = new CryptoStoreCommand(this);
|
||||
command.setExecutor(cryptoStoreCommand);
|
||||
command.setTabCompleter(cryptoStoreCommand);
|
||||
}
|
||||
|
||||
private void registerListeners() {
|
||||
getServer().getPluginManager().registerEvents(new StoreGuiListener(this), this);
|
||||
}
|
||||
|
||||
private void startWatcher() {
|
||||
this.paymentWatcher = new PaymentWatcher(this);
|
||||
|
||||
long pollSeconds = getConfig().getLong("settings.payment.poll-seconds", 30L);
|
||||
long periodTicks = Math.max(20L, pollSeconds * 20L);
|
||||
|
||||
watcherTask = getServer().getScheduler().runTaskTimerAsynchronously(this, paymentWatcher, periodTicks, periodTicks);
|
||||
}
|
||||
|
||||
public void reloadPlugin() {
|
||||
reloadConfig();
|
||||
|
||||
if (watcherTask != null) {
|
||||
watcherTask.cancel();
|
||||
watcherTask = null;
|
||||
}
|
||||
|
||||
configManager.load();
|
||||
|
||||
if (pricingManager != null) {
|
||||
pricingManager.clearCache();
|
||||
}
|
||||
|
||||
invoiceManager.load();
|
||||
|
||||
if (qrMapManager != null) {
|
||||
qrMapManager.clearAll();
|
||||
}
|
||||
|
||||
startWatcher();
|
||||
}
|
||||
|
||||
public static DirtCryptoStorePlugin getInstance() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
public ConfigManager getConfigManager() {
|
||||
return configManager;
|
||||
}
|
||||
|
||||
public InvoiceManager getInvoiceManager() {
|
||||
return invoiceManager;
|
||||
}
|
||||
|
||||
public PaymentWatcher getPaymentWatcher() {
|
||||
return paymentWatcher;
|
||||
}
|
||||
|
||||
public QrMapManager getQrMapManager() {
|
||||
return qrMapManager;
|
||||
}
|
||||
|
||||
public PricingManager getPricingManager() {
|
||||
return pricingManager;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
package com.bitnix.dirtcryptostore.command;
|
||||
|
||||
import com.bitnix.dirtcryptostore.DirtCryptoStorePlugin;
|
||||
import com.bitnix.dirtcryptostore.gui.StoreGui;
|
||||
import com.bitnix.dirtcryptostore.model.BlockchainPaymentMatch;
|
||||
import com.bitnix.dirtcryptostore.model.PendingInvoice;
|
||||
import com.bitnix.dirtcryptostore.model.Product;
|
||||
import com.bitnix.dirtcryptostore.model.WalletConfig;
|
||||
import com.bitnix.dirtcryptostore.util.ColorUtil;
|
||||
import com.bitnix.dirtcryptostore.util.MessageUtil;
|
||||
import com.bitnix.dirtcryptostore.util.TimeUtil;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandExecutor;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.command.TabCompleter;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
public class CryptoStoreCommand implements CommandExecutor, TabCompleter {
|
||||
|
||||
private final DirtCryptoStorePlugin plugin;
|
||||
|
||||
public CryptoStoreCommand(DirtCryptoStorePlugin plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
|
||||
if (args.length > 0) {
|
||||
String sub = args[0].toLowerCase(Locale.ROOT);
|
||||
|
||||
if (sub.equals("reload")) {
|
||||
if (!sender.hasPermission("dirtcryptostore.admin")) {
|
||||
MessageUtil.send(plugin, sender, "messages.no-permission", "&cYou do not have permission.");
|
||||
return true;
|
||||
}
|
||||
|
||||
plugin.reloadPlugin();
|
||||
MessageUtil.send(plugin, sender, "messages.reloaded", "&aDirtCryptoStore reloaded.");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (sub.equals("status")) {
|
||||
if (!(sender instanceof Player player)) {
|
||||
MessageUtil.send(plugin, sender, "messages.player-only", "&cOnly players can use this command.");
|
||||
return true;
|
||||
}
|
||||
|
||||
PendingInvoice invoice = plugin.getInvoiceManager().getPendingInvoice(player.getUniqueId());
|
||||
if (invoice == null) {
|
||||
MessageUtil.send(plugin, player, "messages.no-pending-payment", "&cYou do not have a pending payment.");
|
||||
return true;
|
||||
}
|
||||
|
||||
sendInvoiceStatus(sender, invoice);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (sub.equals("review")) {
|
||||
if (!sender.hasPermission("dirtcryptostore.admin")) {
|
||||
MessageUtil.send(plugin, sender, "messages.no-permission", "&cYou do not have permission.");
|
||||
return true;
|
||||
}
|
||||
|
||||
int shown = 0;
|
||||
Collection<PendingInvoice> all = plugin.getInvoiceManager().getAllInvoices();
|
||||
sender.sendMessage(ColorUtil.color("&6--- DirtCryptoStore Review Queue ---"));
|
||||
for (PendingInvoice invoice : all) {
|
||||
if (invoice.getStatus() != PendingInvoice.Status.UNDER_REVIEW) {
|
||||
continue;
|
||||
}
|
||||
|
||||
shown++;
|
||||
sender.sendMessage(ColorUtil.color("&eInvoice: &f" + invoice.getInvoiceId()));
|
||||
sender.sendMessage(ColorUtil.color("&7Player: &f" + invoice.getPlayerName()));
|
||||
sender.sendMessage(ColorUtil.color("&7Product: &f" + invoice.getProductId()));
|
||||
sender.sendMessage(ColorUtil.color("&7Amount: &f" + TimeUtil.formatBtcAmount(invoice.getAmountSats())));
|
||||
sender.sendMessage(ColorUtil.color("&7TXID: &f" + (invoice.getTxid().isEmpty() ? "N/A" : invoice.getTxid())));
|
||||
sender.sendMessage(ColorUtil.color("&7Notes: &f" + invoice.getNotes()));
|
||||
sender.sendMessage(ColorUtil.color("&7Use: &f/cryptostore forceconfirm " + invoice.getInvoiceId()));
|
||||
sender.sendMessage(ColorUtil.color("&7Use: &f/cryptostore cancelinvoice " + invoice.getInvoiceId()));
|
||||
}
|
||||
|
||||
if (shown == 0) {
|
||||
sender.sendMessage(ColorUtil.color("&aNo invoices are currently under review."));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (sub.equals("forceconfirm")) {
|
||||
if (!sender.hasPermission("dirtcryptostore.admin")) {
|
||||
MessageUtil.send(plugin, sender, "messages.no-permission", "&cYou do not have permission.");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (args.length < 2) {
|
||||
sender.sendMessage(ColorUtil.color("&cUsage: /cryptostore forceconfirm <invoiceId>"));
|
||||
return true;
|
||||
}
|
||||
|
||||
PendingInvoice invoice = plugin.getInvoiceManager().getInvoiceById(args[1]);
|
||||
if (invoice == null) {
|
||||
sender.sendMessage(ColorUtil.color("&cInvoice not found."));
|
||||
return true;
|
||||
}
|
||||
|
||||
Product product = plugin.getConfigManager().getProduct(invoice.getProductId());
|
||||
WalletConfig wallet = plugin.getConfigManager().getWallet(invoice.getWalletId());
|
||||
|
||||
if (product == null) {
|
||||
sender.sendMessage(ColorUtil.color("&cProduct config missing for that invoice."));
|
||||
return true;
|
||||
}
|
||||
|
||||
BlockchainPaymentMatch match = new BlockchainPaymentMatch(
|
||||
true,
|
||||
invoice.getTxid().isEmpty() ? "MANUAL_REVIEW" : invoice.getTxid(),
|
||||
Math.max(invoice.getConfirmations(), wallet == null ? 2 : wallet.getMinConfirmations()),
|
||||
0
|
||||
);
|
||||
|
||||
plugin.getPaymentWatcher().forceFulfillFromAdmin(invoice, match);
|
||||
|
||||
sender.sendMessage(ColorUtil.color("&aInvoice force-confirmed: &f" + invoice.getInvoiceId()));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (sub.equals("cancelinvoice")) {
|
||||
if (!sender.hasPermission("dirtcryptostore.admin")) {
|
||||
MessageUtil.send(plugin, sender, "messages.no-permission", "&cYou do not have permission.");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (args.length < 2) {
|
||||
sender.sendMessage(ColorUtil.color("&cUsage: /cryptostore cancelinvoice <invoiceId>"));
|
||||
return true;
|
||||
}
|
||||
|
||||
PendingInvoice invoice = plugin.getInvoiceManager().getInvoiceById(args[1]);
|
||||
if (invoice == null) {
|
||||
sender.sendMessage(ColorUtil.color("&cInvoice not found."));
|
||||
return true;
|
||||
}
|
||||
|
||||
plugin.getInvoiceManager().expireInvoice(invoice, "Cancelled by admin.");
|
||||
sender.sendMessage(ColorUtil.color("&aInvoice cancelled: &f" + invoice.getInvoiceId()));
|
||||
|
||||
Player player = Bukkit.getPlayer(invoice.getPlayerUuid());
|
||||
if (player != null) {
|
||||
MessageUtil.send(plugin, player, "messages.invoice-cancelled", "&eYour pending payment was cancelled.");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!(sender instanceof Player player)) {
|
||||
MessageUtil.send(plugin, sender, "messages.player-only", "&cOnly players can use this command.");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!player.hasPermission("dirtcryptostore.use")) {
|
||||
MessageUtil.send(plugin, player, "messages.no-permission", "&cYou do not have permission.");
|
||||
return true;
|
||||
}
|
||||
|
||||
MessageUtil.send(plugin, player, "messages.opening-store", "&aOpening crypto store...");
|
||||
new StoreGui(plugin).open(player);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void sendInvoiceStatus(CommandSender sender, PendingInvoice invoice) {
|
||||
String product = invoice.getProductId();
|
||||
String amount = TimeUtil.formatBtcAmount(invoice.getAmountSats());
|
||||
String status = invoice.getStatus().name();
|
||||
String txid = invoice.getTxid().isEmpty() ? "N/A" : invoice.getTxid();
|
||||
String time = TimeUtil.formatRemaining(invoice.getRemainingMillis());
|
||||
|
||||
MessageUtil.send(plugin, sender, "messages.product-line", "&7Product: &f%product%", "%product%", product);
|
||||
MessageUtil.send(plugin, sender, "messages.amount-line", "&7Amount: &f%amount%", "%amount%", amount);
|
||||
MessageUtil.send(plugin, sender, "messages.txid-line", "&7TXID: &f%txid%", "%txid%", txid);
|
||||
sender.sendMessage(ColorUtil.color("&7Status: &f" + status));
|
||||
sender.sendMessage(ColorUtil.color("&7Expires in: &f" + time));
|
||||
sender.sendMessage(ColorUtil.color("&7Notes: &f" + invoice.getNotes()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) {
|
||||
if (args.length == 1) {
|
||||
List<String> suggestions = new ArrayList<>();
|
||||
suggestions.add("status");
|
||||
|
||||
if (sender.hasPermission("dirtcryptostore.admin")) {
|
||||
suggestions.add("reload");
|
||||
suggestions.add("review");
|
||||
suggestions.add("forceconfirm");
|
||||
suggestions.add("cancelinvoice");
|
||||
}
|
||||
|
||||
String input = args[0].toLowerCase(Locale.ROOT);
|
||||
suggestions.removeIf(s -> !s.toLowerCase(Locale.ROOT).startsWith(input));
|
||||
Collections.sort(suggestions);
|
||||
return suggestions;
|
||||
}
|
||||
|
||||
if (args.length == 2 && sender.hasPermission("dirtcryptostore.admin")) {
|
||||
String sub = args[0].toLowerCase(Locale.ROOT);
|
||||
if (sub.equals("forceconfirm") || sub.equals("cancelinvoice")) {
|
||||
List<String> suggestions = new ArrayList<>();
|
||||
for (PendingInvoice invoice : plugin.getInvoiceManager().getAllInvoices()) {
|
||||
suggestions.add(invoice.getInvoiceId());
|
||||
}
|
||||
String input = args[1].toLowerCase(Locale.ROOT);
|
||||
suggestions.removeIf(s -> !s.toLowerCase(Locale.ROOT).startsWith(input));
|
||||
Collections.sort(suggestions);
|
||||
return suggestions;
|
||||
}
|
||||
}
|
||||
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
package com.bitnix.dirtcryptostore.gui;
|
||||
|
||||
import com.bitnix.dirtcryptostore.DirtCryptoStorePlugin;
|
||||
import com.bitnix.dirtcryptostore.model.PendingInvoice;
|
||||
import com.bitnix.dirtcryptostore.model.Product;
|
||||
import com.bitnix.dirtcryptostore.model.WalletConfig;
|
||||
import com.bitnix.dirtcryptostore.util.ColorUtil;
|
||||
import com.bitnix.dirtcryptostore.util.QrMapUtil;
|
||||
import com.bitnix.dirtcryptostore.util.QrPlaceholderUtil;
|
||||
import com.bitnix.dirtcryptostore.util.TimeUtil;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.configuration.ConfigurationSection;
|
||||
import org.bukkit.enchantments.Enchantment;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.inventory.ItemFlag;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class PaymentGui {
|
||||
|
||||
private final DirtCryptoStorePlugin plugin;
|
||||
|
||||
public PaymentGui(DirtCryptoStorePlugin plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
public void open(Player player, PendingInvoice invoice) {
|
||||
String title = ColorUtil.color(plugin.getConfig().getString("gui.payment.title", "&8Crypto Payment"));
|
||||
int size = plugin.getConfig().getInt("gui.payment.size", 27);
|
||||
|
||||
Inventory inventory = Bukkit.createInventory(null, size, title);
|
||||
|
||||
applyStaticItems(inventory);
|
||||
applyDynamicItems(player, inventory, invoice);
|
||||
|
||||
player.openInventory(inventory);
|
||||
}
|
||||
|
||||
private void applyStaticItems(Inventory inventory) {
|
||||
ConfigurationSection itemsSection = plugin.getConfig().getConfigurationSection("gui.payment.items");
|
||||
if (itemsSection == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (String key : itemsSection.getKeys(false)) {
|
||||
ConfigurationSection section = itemsSection.getConfigurationSection(key);
|
||||
if (section == null || !section.getBoolean("enabled", true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (key.equalsIgnoreCase("filler")) {
|
||||
ItemStack filler = createItem(
|
||||
section.getString("material", "BLACK_STAINED_GLASS_PANE"),
|
||||
section.getString("name", " "),
|
||||
section.getStringList("lore"),
|
||||
0,
|
||||
false
|
||||
);
|
||||
|
||||
for (int slot : section.getIntegerList("slots")) {
|
||||
if (slot >= 0 && slot < inventory.getSize()) {
|
||||
inventory.setItem(slot, filler);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void applyDynamicItems(Player player, Inventory inventory, PendingInvoice invoice) {
|
||||
ConfigurationSection itemsSection = plugin.getConfig().getConfigurationSection("gui.payment.items");
|
||||
if (itemsSection == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Product product = plugin.getConfigManager().getProduct(invoice.getProductId());
|
||||
WalletConfig wallet = plugin.getConfigManager().getWallet(invoice.getWalletId());
|
||||
|
||||
String productName = product == null ? invoice.getProductId() : product.getDisplayName();
|
||||
String walletName = wallet == null ? invoice.getWalletId() : wallet.getDisplayName();
|
||||
String amount = TimeUtil.formatBtcAmount(invoice.getAmountSats());
|
||||
String rawAmount = TimeUtil.formatBtcRaw(invoice.getAmountSats());
|
||||
String status = invoice.getStatus().name();
|
||||
String txid = invoice.getTxid() == null || invoice.getTxid().isEmpty() ? "N/A" : invoice.getTxid();
|
||||
String time = TimeUtil.formatRemaining(invoice.getRemainingMillis());
|
||||
String address = invoice.getWalletAddress();
|
||||
int requiredConfirmations = wallet == null
|
||||
? plugin.getConfig().getInt("settings.payment.confirmations-required", 2)
|
||||
: wallet.getMinConfirmations();
|
||||
String bitcoinUri = QrPlaceholderUtil.buildBitcoinUri(invoice);
|
||||
|
||||
placeDynamicItem(inventory, itemsSection, "product",
|
||||
replaceLore(itemsSection, "product", productName, walletName, amount, rawAmount, status, txid, time, address, invoice.getConfirmations(), requiredConfirmations, bitcoinUri));
|
||||
|
||||
placeQrItem(player, inventory, itemsSection, invoice, productName, walletName, amount, rawAmount, status, txid, time, address, requiredConfirmations, bitcoinUri);
|
||||
|
||||
placeDynamicItem(inventory, itemsSection, "status",
|
||||
replaceLore(itemsSection, "status", productName, walletName, amount, rawAmount, status, txid, time, address, invoice.getConfirmations(), requiredConfirmations, bitcoinUri));
|
||||
|
||||
placeConfiguredButton(inventory, itemsSection, "back");
|
||||
placeConfiguredButton(inventory, itemsSection, "cancel");
|
||||
placeConfiguredButton(inventory, itemsSection, "refresh");
|
||||
}
|
||||
|
||||
private void placeQrItem(Player player,
|
||||
Inventory inventory,
|
||||
ConfigurationSection itemsSection,
|
||||
PendingInvoice invoice,
|
||||
String productName,
|
||||
String walletName,
|
||||
String amount,
|
||||
String rawAmount,
|
||||
String status,
|
||||
String txid,
|
||||
String time,
|
||||
String address,
|
||||
int requiredConfirmations,
|
||||
String bitcoinUri) {
|
||||
ConfigurationSection section = itemsSection.getConfigurationSection("qr");
|
||||
if (section == null || !section.getBoolean("enabled", true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
int slot = section.getInt("slot", -1);
|
||||
if (slot < 0 || slot >= inventory.getSize()) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<String> lore = replaceLore(itemsSection, "qr", productName, walletName, amount, rawAmount, status, txid, time, address, invoice.getConfirmations(), requiredConfirmations, bitcoinUri);
|
||||
|
||||
ItemStack qrItem = QrMapUtil.createQrMapItem(
|
||||
plugin,
|
||||
player,
|
||||
bitcoinUri,
|
||||
ColorUtil.color(section.getString("name", "&aScan QR To Pay")),
|
||||
ColorUtil.color(lore),
|
||||
plugin.getConfig().getString("settings.qr.fallback-material-if-map-fails", "PAPER")
|
||||
);
|
||||
|
||||
inventory.setItem(slot, qrItem);
|
||||
}
|
||||
|
||||
private void placeDynamicItem(Inventory inventory, ConfigurationSection itemsSection, String key, List<String> lore) {
|
||||
ConfigurationSection section = itemsSection.getConfigurationSection(key);
|
||||
if (section == null || !section.getBoolean("enabled", true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
int slot = section.getInt("slot", -1);
|
||||
if (slot < 0 || slot >= inventory.getSize()) {
|
||||
return;
|
||||
}
|
||||
|
||||
inventory.setItem(slot, createItem(
|
||||
section.getString("material", "PAPER"),
|
||||
section.getString("name", "&fItem"),
|
||||
lore,
|
||||
section.getInt("custom-model-data", 0),
|
||||
section.getBoolean("glow", false)
|
||||
));
|
||||
}
|
||||
|
||||
private void placeConfiguredButton(Inventory inventory, ConfigurationSection itemsSection, String key) {
|
||||
ConfigurationSection section = itemsSection.getConfigurationSection(key);
|
||||
if (section == null || !section.getBoolean("enabled", true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
int slot = section.getInt("slot", -1);
|
||||
if (slot < 0 || slot >= inventory.getSize()) {
|
||||
return;
|
||||
}
|
||||
|
||||
inventory.setItem(slot, createItem(
|
||||
section.getString("material", "STONE"),
|
||||
section.getString("name", "&fButton"),
|
||||
section.getStringList("lore"),
|
||||
section.getInt("custom-model-data", 0),
|
||||
section.getBoolean("glow", false)
|
||||
));
|
||||
}
|
||||
|
||||
private List<String> replaceLore(ConfigurationSection itemsSection,
|
||||
String key,
|
||||
String product,
|
||||
String wallet,
|
||||
String amount,
|
||||
String rawAmount,
|
||||
String status,
|
||||
String txid,
|
||||
String time,
|
||||
String address,
|
||||
int currentConfirmations,
|
||||
int requiredConfirmations,
|
||||
String bitcoinUri) {
|
||||
ConfigurationSection section = itemsSection.getConfigurationSection(key);
|
||||
if (section == null) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
List<String> lore = new ArrayList<>();
|
||||
for (String line : section.getStringList("lore")) {
|
||||
lore.add(line
|
||||
.replace("%product%", ColorUtil.color(product))
|
||||
.replace("%wallet%", ColorUtil.color(wallet))
|
||||
.replace("%amount%", amount)
|
||||
.replace("%amount_raw%", rawAmount)
|
||||
.replace("%amount_sats%", String.valueOf(parseSatsFromRaw(rawAmount)))
|
||||
.replace("%status%", status)
|
||||
.replace("%txid%", txid)
|
||||
.replace("%time%", time)
|
||||
.replace("%address%", address)
|
||||
.replace("%bitcoin_uri%", bitcoinUri)
|
||||
.replace("%current_confirmations%", String.valueOf(currentConfirmations))
|
||||
.replace("%confirmations%", String.valueOf(requiredConfirmations)));
|
||||
}
|
||||
return lore;
|
||||
}
|
||||
|
||||
private long parseSatsFromRaw(String rawAmount) {
|
||||
String[] split = rawAmount.split("\\.");
|
||||
if (split.length != 2) {
|
||||
return 0L;
|
||||
}
|
||||
|
||||
try {
|
||||
long whole = Long.parseLong(split[0]);
|
||||
String fraction = split[1];
|
||||
while (fraction.length() < 8) {
|
||||
fraction += "0";
|
||||
}
|
||||
if (fraction.length() > 8) {
|
||||
fraction = fraction.substring(0, 8);
|
||||
}
|
||||
long frac = Long.parseLong(fraction);
|
||||
return (whole * 100_000_000L) + frac;
|
||||
} catch (NumberFormatException ex) {
|
||||
return 0L;
|
||||
}
|
||||
}
|
||||
|
||||
private ItemStack createItem(String materialName, String name, List<String> lore, int customModelData, boolean glow) {
|
||||
Material material = Material.matchMaterial(materialName);
|
||||
if (material == null) {
|
||||
material = Material.STONE;
|
||||
}
|
||||
|
||||
ItemStack item = new ItemStack(material);
|
||||
ItemMeta meta = item.getItemMeta();
|
||||
if (meta == null) {
|
||||
return item;
|
||||
}
|
||||
|
||||
meta.setDisplayName(ColorUtil.color(name));
|
||||
meta.setLore(ColorUtil.color(lore));
|
||||
|
||||
if (customModelData > 0) {
|
||||
meta.setCustomModelData(customModelData);
|
||||
}
|
||||
|
||||
if (glow) {
|
||||
meta.addEnchant(Enchantment.LUCK_OF_THE_SEA, 1, true);
|
||||
meta.addItemFlags(ItemFlag.HIDE_ENCHANTS);
|
||||
}
|
||||
|
||||
item.setItemMeta(meta);
|
||||
return item;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
package com.bitnix.dirtcryptostore.gui;
|
||||
|
||||
import com.bitnix.dirtcryptostore.DirtCryptoStorePlugin;
|
||||
import com.bitnix.dirtcryptostore.model.Product;
|
||||
import com.bitnix.dirtcryptostore.model.WalletConfig;
|
||||
import com.bitnix.dirtcryptostore.util.ColorUtil;
|
||||
import com.bitnix.dirtcryptostore.util.TimeUtil;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.configuration.ConfigurationSection;
|
||||
import org.bukkit.enchantments.Enchantment;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.inventory.ItemFlag;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class StoreGui {
|
||||
|
||||
private final DirtCryptoStorePlugin plugin;
|
||||
|
||||
public StoreGui(DirtCryptoStorePlugin plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
public void open(Player player) {
|
||||
String title = ColorUtil.color(plugin.getConfig().getString("gui.store.title", "&8Crypto Store"));
|
||||
int size = plugin.getConfig().getInt("gui.store.size", 27);
|
||||
|
||||
Inventory inventory = Bukkit.createInventory(null, size, title);
|
||||
|
||||
applyStaticItems(inventory);
|
||||
applyProducts(player, inventory);
|
||||
|
||||
player.openInventory(inventory);
|
||||
}
|
||||
|
||||
private void applyStaticItems(Inventory inventory) {
|
||||
ConfigurationSection itemsSection = plugin.getConfig().getConfigurationSection("gui.store.items");
|
||||
if (itemsSection == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (String key : itemsSection.getKeys(false)) {
|
||||
ConfigurationSection section = itemsSection.getConfigurationSection(key);
|
||||
if (section == null || !section.getBoolean("enabled", true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (key.equalsIgnoreCase("filler")) {
|
||||
ItemStack filler = createItem(
|
||||
section.getString("material", "GRAY_STAINED_GLASS_PANE"),
|
||||
section.getString("name", " "),
|
||||
section.getStringList("lore"),
|
||||
0,
|
||||
false
|
||||
);
|
||||
|
||||
for (int slot : section.getIntegerList("slots")) {
|
||||
if (slot >= 0 && slot < inventory.getSize()) {
|
||||
inventory.setItem(slot, filler);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
int slot = section.getInt("slot", -1);
|
||||
if (slot < 0 || slot >= inventory.getSize()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
inventory.setItem(slot, createItem(
|
||||
section.getString("material", "STONE"),
|
||||
section.getString("name", "&fItem"),
|
||||
section.getStringList("lore"),
|
||||
section.getInt("custom-model-data", 0),
|
||||
section.getBoolean("glow", false)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
private void applyProducts(Player player, Inventory inventory) {
|
||||
for (Product product : plugin.getConfigManager().getProducts()) {
|
||||
if (!product.isEnabled()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!product.getPermissionRequired().isEmpty() && !player.hasPermission(product.getPermissionRequired())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (product.getSlot() < 0 || product.getSlot() >= inventory.getSize()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
WalletConfig wallet = plugin.getConfigManager().getWallet(product.getWallet());
|
||||
if (wallet == null || !wallet.isEnabled()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!plugin.getConfigManager().isWalletAllowedForProduct(wallet, product.getId())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
List<String> lore = new ArrayList<>();
|
||||
for (String line : product.getLore()) {
|
||||
lore.add(line
|
||||
.replace("%wallet%", ColorUtil.color(wallet.getDisplayName()))
|
||||
.replace("%wallet_address%", wallet.getAddress())
|
||||
.replace("%amount%", TimeUtil.formatBtcAmount(product.getBaseSats()))
|
||||
.replace("%amount_sats%", String.valueOf(product.getBaseSats()))
|
||||
.replace("%price_usd%", String.format("%.2f", product.getPriceUsd()))
|
||||
.replace("%product%", ColorUtil.color(product.getDisplayName())));
|
||||
}
|
||||
|
||||
inventory.setItem(product.getSlot(), createItem(
|
||||
product.getMaterialName(),
|
||||
product.getDisplayName(),
|
||||
lore,
|
||||
product.getCustomModelData(),
|
||||
product.isGlow()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
private ItemStack createItem(String materialName, String name, List<String> lore, int customModelData, boolean glow) {
|
||||
Material material = Material.matchMaterial(materialName);
|
||||
if (material == null) {
|
||||
material = Material.STONE;
|
||||
}
|
||||
|
||||
ItemStack item = new ItemStack(material);
|
||||
ItemMeta meta = item.getItemMeta();
|
||||
if (meta == null) {
|
||||
return item;
|
||||
}
|
||||
|
||||
meta.setDisplayName(ColorUtil.color(name));
|
||||
meta.setLore(ColorUtil.color(lore));
|
||||
|
||||
if (customModelData > 0) {
|
||||
meta.setCustomModelData(customModelData);
|
||||
}
|
||||
|
||||
if (glow) {
|
||||
meta.addEnchant(Enchantment.LUCK_OF_THE_SEA, 1, true);
|
||||
meta.addItemFlags(ItemFlag.HIDE_ENCHANTS);
|
||||
}
|
||||
|
||||
item.setItemMeta(meta);
|
||||
return item;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
package com.bitnix.dirtcryptostore.listener;
|
||||
|
||||
import com.bitnix.dirtcryptostore.DirtCryptoStorePlugin;
|
||||
import com.bitnix.dirtcryptostore.gui.PaymentGui;
|
||||
import com.bitnix.dirtcryptostore.gui.StoreGui;
|
||||
import com.bitnix.dirtcryptostore.model.PendingInvoice;
|
||||
import com.bitnix.dirtcryptostore.model.Product;
|
||||
import com.bitnix.dirtcryptostore.model.WalletConfig;
|
||||
import com.bitnix.dirtcryptostore.util.ColorUtil;
|
||||
import com.bitnix.dirtcryptostore.util.EffectUtil;
|
||||
import com.bitnix.dirtcryptostore.util.MessageUtil;
|
||||
import com.bitnix.dirtcryptostore.util.TitleUtil;
|
||||
import org.bukkit.Sound;
|
||||
import org.bukkit.configuration.ConfigurationSection;
|
||||
import org.bukkit.entity.HumanEntity;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.inventory.InventoryClickEvent;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
|
||||
public class StoreGuiListener implements Listener {
|
||||
|
||||
private final DirtCryptoStorePlugin plugin;
|
||||
|
||||
public StoreGuiListener(DirtCryptoStorePlugin plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onInventoryClick(InventoryClickEvent event) {
|
||||
String title = event.getView().getTitle();
|
||||
String storeTitle = ColorUtil.color(plugin.getConfig().getString("gui.store.title", "&8Crypto Store"));
|
||||
String paymentTitle = ColorUtil.color(plugin.getConfig().getString("gui.payment.title", "&8Crypto Payment"));
|
||||
|
||||
if (title.equals(storeTitle)) {
|
||||
handleStoreClick(event);
|
||||
return;
|
||||
}
|
||||
|
||||
if (title.equals(paymentTitle)) {
|
||||
handlePaymentClick(event);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleStoreClick(InventoryClickEvent event) {
|
||||
event.setCancelled(true);
|
||||
|
||||
HumanEntity clicker = event.getWhoClicked();
|
||||
if (!(clicker instanceof Player player)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.getRawSlot() < 0 || event.getRawSlot() >= event.getInventory().getSize()) {
|
||||
return;
|
||||
}
|
||||
|
||||
ItemStack clicked = event.getCurrentItem();
|
||||
if (clicked == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
ConfigurationSection storeItems = plugin.getConfig().getConfigurationSection("gui.store.items");
|
||||
if (storeItems != null) {
|
||||
ConfigurationSection close = storeItems.getConfigurationSection("close");
|
||||
if (close != null && close.getBoolean("enabled", true) && event.getRawSlot() == close.getInt("slot", -1)) {
|
||||
playConfiguredSound(player, "click");
|
||||
player.closeInventory();
|
||||
return;
|
||||
}
|
||||
|
||||
ConfigurationSection refresh = storeItems.getConfigurationSection("refresh");
|
||||
if (refresh != null && refresh.getBoolean("enabled", true) && event.getRawSlot() == refresh.getInt("slot", -1)) {
|
||||
playConfiguredSound(player, "click");
|
||||
new StoreGui(plugin).open(player);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
for (Product product : plugin.getConfigManager().getProducts()) {
|
||||
if (!product.isEnabled()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (product.getSlot() != event.getRawSlot()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!product.getPermissionRequired().isEmpty() && !player.hasPermission(product.getPermissionRequired())) {
|
||||
MessageUtil.send(plugin, player, "messages.no-permission", "&cYou do not have permission.");
|
||||
return;
|
||||
}
|
||||
|
||||
PendingInvoice existing = plugin.getInvoiceManager().getPendingInvoice(player.getUniqueId());
|
||||
if (existing != null) {
|
||||
plugin.getQrMapManager().giveToOffhand(player, existing);
|
||||
MessageUtil.send(plugin, player, "messages.already-has-pending", "&cYou already have a pending payment.");
|
||||
player.sendMessage(ColorUtil.color("&aYour QR payment map has been placed in your off-hand."));
|
||||
new PaymentGui(plugin).open(player, existing);
|
||||
return;
|
||||
}
|
||||
|
||||
WalletConfig wallet = plugin.getConfigManager().getWallet(product.getWallet());
|
||||
if (wallet == null || !wallet.isEnabled() || wallet.getAddress().isEmpty()) {
|
||||
MessageUtil.send(plugin, player, "messages.payment-under-review", "&eA matching payment was found but needs review.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!plugin.getConfigManager().isWalletAllowedForProduct(wallet, product.getId())) {
|
||||
MessageUtil.send(plugin, player, "messages.payment-under-review", "&eA matching payment was found but needs review.");
|
||||
return;
|
||||
}
|
||||
|
||||
long previewBaseSats = plugin.getPricingManager().resolveBaseSats(product);
|
||||
if (previewBaseSats <= 0L) {
|
||||
MessageUtil.send(plugin, player, "messages.pricing-unavailable", "&cLive BTC pricing is currently unavailable. Please try again in a moment.");
|
||||
return;
|
||||
}
|
||||
|
||||
PendingInvoice invoice = plugin.getInvoiceManager().createInvoice(player, product, wallet);
|
||||
if (invoice == null) {
|
||||
MessageUtil.send(plugin, player, "messages.pricing-unavailable", "&cLive BTC pricing is currently unavailable. Please try again in a moment.");
|
||||
return;
|
||||
}
|
||||
|
||||
plugin.getQrMapManager().giveToOffhand(player, invoice);
|
||||
|
||||
playConfiguredSound(player, "invoice-created");
|
||||
EffectUtil.playEffect(plugin, player, "invoice-created");
|
||||
TitleUtil.sendTitle(plugin, player, "invoice-created");
|
||||
MessageUtil.send(plugin, player, "messages.invoice-created", "&aInvoice created for &f%product%&a.",
|
||||
"%product%", product.getDisplayName());
|
||||
player.sendMessage(ColorUtil.color("&aYour QR payment map has been placed in your off-hand."));
|
||||
|
||||
new PaymentGui(plugin).open(player, invoice);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private void handlePaymentClick(InventoryClickEvent event) {
|
||||
event.setCancelled(true);
|
||||
|
||||
HumanEntity clicker = event.getWhoClicked();
|
||||
if (!(clicker instanceof Player player)) {
|
||||
return;
|
||||
}
|
||||
|
||||
PendingInvoice invoice = plugin.getInvoiceManager().getPendingInvoice(player.getUniqueId());
|
||||
if (invoice == null) {
|
||||
MessageUtil.send(plugin, player, "messages.no-pending-payment", "&cYou do not have a pending payment.");
|
||||
player.closeInventory();
|
||||
return;
|
||||
}
|
||||
|
||||
ConfigurationSection paymentItems = plugin.getConfig().getConfigurationSection("gui.payment.items");
|
||||
if (paymentItems == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
int slot = event.getRawSlot();
|
||||
|
||||
ConfigurationSection back = paymentItems.getConfigurationSection("back");
|
||||
if (back != null && back.getBoolean("enabled", true) && slot == back.getInt("slot", -1)) {
|
||||
playConfiguredSound(player, "click");
|
||||
new StoreGui(plugin).open(player);
|
||||
return;
|
||||
}
|
||||
|
||||
ConfigurationSection cancel = paymentItems.getConfigurationSection("cancel");
|
||||
if (cancel != null && cancel.getBoolean("enabled", true) && slot == cancel.getInt("slot", -1)) {
|
||||
if (!plugin.getConfig().getBoolean("settings.payment.allow-cancel", true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
plugin.getInvoiceManager().cancelInvoice(player.getUniqueId());
|
||||
plugin.getQrMapManager().clear(player);
|
||||
playConfiguredSound(player, "cancelled");
|
||||
EffectUtil.playEffect(plugin, player, "cancelled");
|
||||
TitleUtil.sendTitle(plugin, player, "cancelled");
|
||||
MessageUtil.send(plugin, player, "messages.invoice-cancelled", "&eYour pending payment was cancelled.");
|
||||
new StoreGui(plugin).open(player);
|
||||
return;
|
||||
}
|
||||
|
||||
ConfigurationSection refresh = paymentItems.getConfigurationSection("refresh");
|
||||
if (refresh != null && refresh.getBoolean("enabled", true) && slot == refresh.getInt("slot", -1)) {
|
||||
playConfiguredSound(player, "click");
|
||||
plugin.getQrMapManager().giveToOffhand(player, invoice);
|
||||
player.sendMessage(ColorUtil.color("&aYour QR payment map has been refreshed in your off-hand."));
|
||||
PendingInvoice refreshed = plugin.getInvoiceManager().getPendingInvoice(player.getUniqueId());
|
||||
if (refreshed != null) {
|
||||
new PaymentGui(plugin).open(player, refreshed);
|
||||
} else {
|
||||
player.closeInventory();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void playConfiguredSound(Player player, String key) {
|
||||
if (!plugin.getConfig().getBoolean("sounds.enabled", true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
ConfigurationSection section = plugin.getConfig().getConfigurationSection("sounds." + key);
|
||||
if (section == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Sound sound = Sound.valueOf(section.getString("sound", "UI_BUTTON_CLICK"));
|
||||
float volume = (float) section.getDouble("volume", 1.0D);
|
||||
float pitch = (float) section.getDouble("pitch", 1.0D);
|
||||
player.playSound(player.getLocation(), sound, volume, pitch);
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package com.bitnix.dirtcryptostore.manager;
|
||||
|
||||
import com.bitnix.dirtcryptostore.DirtCryptoStorePlugin;
|
||||
import com.bitnix.dirtcryptostore.model.BlockchainPaymentMatch;
|
||||
import com.bitnix.dirtcryptostore.model.PendingInvoice;
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
|
||||
public class BlockchainApi {
|
||||
|
||||
private final DirtCryptoStorePlugin plugin;
|
||||
|
||||
public BlockchainApi(DirtCryptoStorePlugin plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
public BlockchainPaymentMatch findPayment(PendingInvoice invoice) {
|
||||
try {
|
||||
int timeout = plugin.getConfig().getInt("settings.blockchain.request-timeout-seconds", 10);
|
||||
String userAgent = plugin.getConfig().getString("settings.blockchain.user-agent", "DirtCryptoStore/1.0.0");
|
||||
|
||||
String latestBlockJson = com.bitnix.dirtcryptostore.util.HttpUtil.get(
|
||||
"https://blockchain.info/latestblock",
|
||||
timeout,
|
||||
userAgent
|
||||
);
|
||||
|
||||
JsonObject latestBlockObject = JsonParser.parseString(latestBlockJson).getAsJsonObject();
|
||||
int latestHeight = latestBlockObject.has("height") ? latestBlockObject.get("height").getAsInt() : 0;
|
||||
|
||||
String address = invoice.getWalletAddress();
|
||||
String rawAddrJson = com.bitnix.dirtcryptostore.util.HttpUtil.get(
|
||||
"https://blockchain.info/rawaddr/" + address,
|
||||
timeout,
|
||||
userAgent
|
||||
);
|
||||
|
||||
JsonObject root = JsonParser.parseString(rawAddrJson).getAsJsonObject();
|
||||
if (!root.has("txs") || !root.get("txs").isJsonArray()) {
|
||||
return new BlockchainPaymentMatch(false, "", 0, 0);
|
||||
}
|
||||
|
||||
JsonArray txs = root.getAsJsonArray("txs");
|
||||
|
||||
for (JsonElement txElement : txs) {
|
||||
if (!txElement.isJsonObject()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
JsonObject tx = txElement.getAsJsonObject();
|
||||
String txid = tx.has("hash") ? tx.get("hash").getAsString() : "";
|
||||
int blockHeight = tx.has("block_height") ? tx.get("block_height").getAsInt() : 0;
|
||||
|
||||
if (!tx.has("out") || !tx.get("out").isJsonArray()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
JsonArray outs = tx.getAsJsonArray("out");
|
||||
for (JsonElement outElement : outs) {
|
||||
if (!outElement.isJsonObject()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
JsonObject out = outElement.getAsJsonObject();
|
||||
|
||||
if (!out.has("value")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
long value = out.get("value").getAsLong();
|
||||
|
||||
String outAddress = "";
|
||||
if (out.has("addr") && !out.get("addr").isJsonNull()) {
|
||||
outAddress = out.get("addr").getAsString();
|
||||
}
|
||||
|
||||
if (!outAddress.equalsIgnoreCase(address)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (value != invoice.getAmountSats()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int confirmations = 0;
|
||||
if (blockHeight > 0 && latestHeight > 0) {
|
||||
confirmations = Math.max(1, (latestHeight - blockHeight) + 1);
|
||||
}
|
||||
|
||||
if (plugin.getConfig().getBoolean("settings.debug", false)) {
|
||||
plugin.getLogger().info("Matched invoice " + invoice.getInvoiceId()
|
||||
+ " to txid=" + txid
|
||||
+ " confirmations=" + confirmations
|
||||
+ " blockHeight=" + blockHeight);
|
||||
}
|
||||
|
||||
return new BlockchainPaymentMatch(true, txid, confirmations, blockHeight);
|
||||
}
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
if (plugin.getConfig().getBoolean("settings.debug", false)) {
|
||||
plugin.getLogger().warning("Blockchain lookup failed for invoice " + invoice.getInvoiceId() + ": " + ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return new BlockchainPaymentMatch(false, "", 0, 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package com.bitnix.dirtcryptostore.manager;
|
||||
|
||||
import com.bitnix.dirtcryptostore.DirtCryptoStorePlugin;
|
||||
import com.bitnix.dirtcryptostore.model.Product;
|
||||
import com.bitnix.dirtcryptostore.model.WalletConfig;
|
||||
import org.bukkit.configuration.ConfigurationSection;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class ConfigManager {
|
||||
|
||||
private final DirtCryptoStorePlugin plugin;
|
||||
private final Map<String, Product> products = new LinkedHashMap<>();
|
||||
private final Map<String, WalletConfig> wallets = new LinkedHashMap<>();
|
||||
|
||||
public ConfigManager(DirtCryptoStorePlugin plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
public void load() {
|
||||
products.clear();
|
||||
wallets.clear();
|
||||
loadWallets();
|
||||
loadProducts();
|
||||
}
|
||||
|
||||
private void loadWallets() {
|
||||
ConfigurationSection section = plugin.getConfig().getConfigurationSection("wallets");
|
||||
if (section == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (String key : section.getKeys(false)) {
|
||||
ConfigurationSection walletSection = section.getConfigurationSection(key);
|
||||
if (walletSection == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
WalletConfig wallet = new WalletConfig(
|
||||
key,
|
||||
walletSection.getBoolean("enabled", true),
|
||||
walletSection.getString("display-name", key),
|
||||
walletSection.getString("coin", "BTC"),
|
||||
walletSection.getString("address", ""),
|
||||
walletSection.getString("explorer-type", "BTC"),
|
||||
walletSection.getInt("min-confirmations", 2),
|
||||
walletSection.getStringList("allow-products")
|
||||
);
|
||||
|
||||
wallets.put(key.toLowerCase(), wallet);
|
||||
}
|
||||
}
|
||||
|
||||
private void loadProducts() {
|
||||
ConfigurationSection section = plugin.getConfig().getConfigurationSection("products");
|
||||
if (section == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (String key : section.getKeys(false)) {
|
||||
ConfigurationSection productSection = section.getConfigurationSection(key);
|
||||
if (productSection == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Product product = new Product(
|
||||
key,
|
||||
productSection.getBoolean("enabled", true),
|
||||
productSection.getString("display-name", key),
|
||||
productSection.getString("material", "STONE"),
|
||||
productSection.getInt("custom-model-data", 0),
|
||||
productSection.getBoolean("glow", false),
|
||||
productSection.getInt("slot", -1),
|
||||
productSection.getString("wallet", "default"),
|
||||
productSection.getDouble("price-usd", 0.0D),
|
||||
productSection.getLong("base-sats", 0L),
|
||||
productSection.getString("permission-required", ""),
|
||||
productSection.getBoolean("purchasable-once", false),
|
||||
productSection.getStringList("commands"),
|
||||
productSection.getStringList("lore")
|
||||
);
|
||||
|
||||
products.put(key.toLowerCase(), product);
|
||||
}
|
||||
}
|
||||
|
||||
public Collection<Product> getProducts() {
|
||||
return Collections.unmodifiableCollection(products.values());
|
||||
}
|
||||
|
||||
public Product getProduct(String id) {
|
||||
if (id == null) {
|
||||
return null;
|
||||
}
|
||||
return products.get(id.toLowerCase());
|
||||
}
|
||||
|
||||
public WalletConfig getWallet(String id) {
|
||||
if (id == null) {
|
||||
return null;
|
||||
}
|
||||
return wallets.get(id.toLowerCase());
|
||||
}
|
||||
|
||||
public boolean isWalletAllowedForProduct(WalletConfig wallet, String productId) {
|
||||
if (wallet == null || productId == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (wallet.getAllowProducts().isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (String allowed : wallet.getAllowProducts()) {
|
||||
if (allowed.equalsIgnoreCase(productId)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,312 @@
|
||||
package com.bitnix.dirtcryptostore.manager;
|
||||
|
||||
import com.bitnix.dirtcryptostore.DirtCryptoStorePlugin;
|
||||
import com.bitnix.dirtcryptostore.model.PendingInvoice;
|
||||
import com.bitnix.dirtcryptostore.model.Product;
|
||||
import com.bitnix.dirtcryptostore.model.WalletConfig;
|
||||
import com.bitnix.dirtcryptostore.storage.SQLiteStorage;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
|
||||
public class InvoiceManager {
|
||||
|
||||
private final DirtCryptoStorePlugin plugin;
|
||||
private final SQLiteStorage storage;
|
||||
|
||||
private final Map<UUID, PendingInvoice> pendingByPlayer = new HashMap<>();
|
||||
private final Map<String, PendingInvoice> allByInvoiceId = new HashMap<>();
|
||||
private final Map<String, String> invoiceIdByTxid = new HashMap<>();
|
||||
private final Set<Long> reservedActiveAmounts = new HashSet<>();
|
||||
|
||||
private long sequentialCounter = 1L;
|
||||
|
||||
public InvoiceManager(DirtCryptoStorePlugin plugin) {
|
||||
this.plugin = plugin;
|
||||
this.storage = new SQLiteStorage(plugin);
|
||||
}
|
||||
|
||||
public synchronized void init() {
|
||||
storage.init();
|
||||
}
|
||||
|
||||
public synchronized void load() {
|
||||
clearAllInternal(false);
|
||||
|
||||
List<PendingInvoice> invoices = storage.loadAllInvoices();
|
||||
for (PendingInvoice invoice : invoices) {
|
||||
allByInvoiceId.put(invoice.getInvoiceId(), invoice);
|
||||
|
||||
if (!invoice.isFinalState()) {
|
||||
pendingByPlayer.put(invoice.getPlayerUuid(), invoice);
|
||||
reservedActiveAmounts.add(invoice.getAmountSats());
|
||||
}
|
||||
|
||||
if (!invoice.getTxid().isEmpty()) {
|
||||
invoiceIdByTxid.put(invoice.getTxid(), invoice.getInvoiceId());
|
||||
}
|
||||
}
|
||||
|
||||
pruneExpiredAndOld();
|
||||
}
|
||||
|
||||
public synchronized void save() {
|
||||
for (PendingInvoice invoice : allByInvoiceId.values()) {
|
||||
storage.saveInvoice(invoice);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized PendingInvoice getPendingInvoice(UUID uuid) {
|
||||
PendingInvoice invoice = pendingByPlayer.get(uuid);
|
||||
if (invoice == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (invoice.getStatus() == PendingInvoice.Status.PENDING && invoice.isExpired()) {
|
||||
expireInvoice(invoice, "Expired while checking pending invoice.");
|
||||
return null;
|
||||
}
|
||||
|
||||
return invoice.isFinalState() ? null : invoice;
|
||||
}
|
||||
|
||||
public synchronized PendingInvoice getInvoiceById(String invoiceId) {
|
||||
return allByInvoiceId.get(invoiceId);
|
||||
}
|
||||
|
||||
public synchronized boolean isTxidAlreadyClaimed(String txid, String invoiceId) {
|
||||
String existing = invoiceIdByTxid.get(txid);
|
||||
return existing != null && !existing.equalsIgnoreCase(invoiceId);
|
||||
}
|
||||
|
||||
public synchronized PendingInvoice createInvoice(Player player, Product product, WalletConfig wallet) {
|
||||
PendingInvoice existing = getPendingInvoice(player.getUniqueId());
|
||||
if (existing != null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
long resolvedBaseSats = plugin.getPricingManager().resolveBaseSats(product);
|
||||
if (resolvedBaseSats <= 0L) {
|
||||
return null;
|
||||
}
|
||||
|
||||
long amountSats = generateUniqueAmount(resolvedBaseSats);
|
||||
long now = System.currentTimeMillis();
|
||||
long expireMinutes = plugin.getConfig().getLong("settings.payment.invoice-expire-minutes", 30L);
|
||||
long expiresAt = now + (expireMinutes * 60_000L);
|
||||
|
||||
PendingInvoice invoice = new PendingInvoice(
|
||||
UUID.randomUUID().toString(),
|
||||
player.getUniqueId(),
|
||||
player.getName(),
|
||||
product.getId(),
|
||||
wallet.getId(),
|
||||
wallet.getAddress(),
|
||||
amountSats,
|
||||
now,
|
||||
expiresAt,
|
||||
PendingInvoice.Status.PENDING,
|
||||
"",
|
||||
0,
|
||||
false,
|
||||
0L,
|
||||
0L,
|
||||
"Invoice created."
|
||||
);
|
||||
|
||||
pendingByPlayer.put(player.getUniqueId(), invoice);
|
||||
allByInvoiceId.put(invoice.getInvoiceId(), invoice);
|
||||
reservedActiveAmounts.add(amountSats);
|
||||
storage.saveInvoice(invoice);
|
||||
return invoice;
|
||||
}
|
||||
|
||||
public synchronized void cancelInvoice(UUID uuid) {
|
||||
PendingInvoice invoice = pendingByPlayer.get(uuid);
|
||||
if (invoice == null || invoice.isFinalState()) {
|
||||
return;
|
||||
}
|
||||
|
||||
invoice.setStatus(PendingInvoice.Status.CANCELLED);
|
||||
invoice.setNotes("Cancelled by player.");
|
||||
reservedActiveAmounts.remove(invoice.getAmountSats());
|
||||
pendingByPlayer.remove(uuid);
|
||||
storage.saveInvoice(invoice);
|
||||
}
|
||||
|
||||
public synchronized void expireInvoice(PendingInvoice invoice, String note) {
|
||||
if (invoice == null || invoice.isFinalState()) {
|
||||
return;
|
||||
}
|
||||
|
||||
invoice.setStatus(PendingInvoice.Status.EXPIRED);
|
||||
invoice.setNotes(note == null ? "Expired." : note);
|
||||
reservedActiveAmounts.remove(invoice.getAmountSats());
|
||||
pendingByPlayer.remove(invoice.getPlayerUuid());
|
||||
storage.saveInvoice(invoice);
|
||||
}
|
||||
|
||||
public synchronized void markDetected(PendingInvoice invoice, String txid, int confirmations, String note) {
|
||||
if (invoice == null || invoice.isFinalState()) {
|
||||
return;
|
||||
}
|
||||
|
||||
invoice.setStatus(PendingInvoice.Status.DETECTED);
|
||||
invoice.setTxid(txid);
|
||||
invoice.setConfirmations(confirmations);
|
||||
if (invoice.getMatchedAt() <= 0L) {
|
||||
invoice.setMatchedAt(System.currentTimeMillis());
|
||||
}
|
||||
invoice.setNotes(note == null ? "Payment detected." : note);
|
||||
|
||||
if (txid != null && !txid.isEmpty()) {
|
||||
invoiceIdByTxid.put(txid, invoice.getInvoiceId());
|
||||
}
|
||||
|
||||
storage.saveInvoice(invoice);
|
||||
}
|
||||
|
||||
public synchronized void markFulfilled(PendingInvoice invoice, String txid, int confirmations, String note) {
|
||||
if (invoice == null || invoice.isFulfilled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
invoice.setStatus(PendingInvoice.Status.FULFILLED);
|
||||
invoice.setTxid(txid);
|
||||
invoice.setConfirmations(confirmations);
|
||||
invoice.setFulfilled(true);
|
||||
if (invoice.getMatchedAt() <= 0L) {
|
||||
invoice.setMatchedAt(System.currentTimeMillis());
|
||||
}
|
||||
invoice.setFulfilledAt(System.currentTimeMillis());
|
||||
invoice.setNotes(note == null ? "Payment fulfilled." : note);
|
||||
|
||||
if (txid != null && !txid.isEmpty()) {
|
||||
invoiceIdByTxid.put(txid, invoice.getInvoiceId());
|
||||
}
|
||||
|
||||
reservedActiveAmounts.remove(invoice.getAmountSats());
|
||||
pendingByPlayer.remove(invoice.getPlayerUuid());
|
||||
storage.saveInvoice(invoice);
|
||||
}
|
||||
|
||||
public synchronized void markUnderReview(PendingInvoice invoice, String txid, int confirmations, String note) {
|
||||
if (invoice == null || invoice.isFinalState()) {
|
||||
return;
|
||||
}
|
||||
|
||||
invoice.setStatus(PendingInvoice.Status.UNDER_REVIEW);
|
||||
invoice.setTxid(txid == null ? "" : txid);
|
||||
invoice.setConfirmations(confirmations);
|
||||
if (invoice.getMatchedAt() <= 0L && txid != null && !txid.isEmpty()) {
|
||||
invoice.setMatchedAt(System.currentTimeMillis());
|
||||
}
|
||||
invoice.setNotes(note == null ? "Marked under review." : note);
|
||||
|
||||
if (txid != null && !txid.isEmpty()) {
|
||||
invoiceIdByTxid.put(txid, invoice.getInvoiceId());
|
||||
}
|
||||
|
||||
reservedActiveAmounts.remove(invoice.getAmountSats());
|
||||
pendingByPlayer.remove(invoice.getPlayerUuid());
|
||||
storage.saveInvoice(invoice);
|
||||
}
|
||||
|
||||
public synchronized Collection<PendingInvoice> getAllInvoices() {
|
||||
return new ArrayList<>(allByInvoiceId.values());
|
||||
}
|
||||
|
||||
public synchronized void cleanupExpiredInvoices() {
|
||||
for (PendingInvoice invoice : new ArrayList<>(allByInvoiceId.values())) {
|
||||
if (invoice.getStatus() == PendingInvoice.Status.PENDING && invoice.isExpired()) {
|
||||
expireInvoice(invoice, "Expired by cleanup task.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void pruneExpiredAndOld() {
|
||||
cleanupExpiredInvoices();
|
||||
|
||||
if (!plugin.getConfig().getBoolean("settings.storage.prune-enabled", true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
int pruneAfterDays = plugin.getConfig().getInt("settings.storage.prune-after-days", 30);
|
||||
long cutoff = System.currentTimeMillis() - (pruneAfterDays * 24L * 60L * 60L * 1000L);
|
||||
|
||||
List<String> statuses = plugin.getConfig().getStringList("settings.storage.prune-statuses");
|
||||
if (statuses.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<String> removeIds = new ArrayList<>();
|
||||
for (PendingInvoice invoice : allByInvoiceId.values()) {
|
||||
if (invoice.getCreatedAt() >= cutoff) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (statuses.contains(invoice.getStatus().name())) {
|
||||
removeIds.add(invoice.getInvoiceId());
|
||||
}
|
||||
}
|
||||
|
||||
for (String invoiceId : removeIds) {
|
||||
PendingInvoice removed = allByInvoiceId.remove(invoiceId);
|
||||
if (removed != null) {
|
||||
pendingByPlayer.remove(removed.getPlayerUuid());
|
||||
reservedActiveAmounts.remove(removed.getAmountSats());
|
||||
if (!removed.getTxid().isEmpty()) {
|
||||
invoiceIdByTxid.remove(removed.getTxid());
|
||||
}
|
||||
storage.deleteInvoice(invoiceId);
|
||||
}
|
||||
}
|
||||
|
||||
storage.pruneOldInvoices(cutoff, statuses);
|
||||
}
|
||||
|
||||
public synchronized void clearAll() {
|
||||
clearAllInternal(true);
|
||||
}
|
||||
|
||||
private void clearAllInternal(boolean resetCounter) {
|
||||
pendingByPlayer.clear();
|
||||
allByInvoiceId.clear();
|
||||
invoiceIdByTxid.clear();
|
||||
reservedActiveAmounts.clear();
|
||||
if (resetCounter) {
|
||||
sequentialCounter = 1L;
|
||||
}
|
||||
}
|
||||
|
||||
private long generateUniqueAmount(long baseSats) {
|
||||
String mode = plugin.getConfig().getString("settings.micro-amount.mode", "RANDOM_RANGE");
|
||||
|
||||
if ("SEQUENTIAL".equalsIgnoreCase(mode)) {
|
||||
long candidate = baseSats + Math.max(1L, sequentialCounter++);
|
||||
while (reservedActiveAmounts.contains(candidate)) {
|
||||
candidate++;
|
||||
}
|
||||
return candidate;
|
||||
}
|
||||
|
||||
long min = plugin.getConfig().getLong("settings.micro-amount.min-extra-sats", 1L);
|
||||
long max = plugin.getConfig().getLong("settings.micro-amount.max-extra-sats", 200L);
|
||||
long attempts = plugin.getConfig().getLong("settings.micro-amount.max-generation-attempts", 500L);
|
||||
|
||||
for (int i = 0; i < attempts; i++) {
|
||||
long randomExtra = ThreadLocalRandom.current().nextLong(min, max + 1);
|
||||
long candidate = baseSats + randomExtra;
|
||||
if (!reservedActiveAmounts.contains(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
long candidate = baseSats + max + 1L;
|
||||
while (reservedActiveAmounts.contains(candidate)) {
|
||||
candidate++;
|
||||
}
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
package com.bitnix.dirtcryptostore.manager;
|
||||
|
||||
import com.bitnix.dirtcryptostore.DirtCryptoStorePlugin;
|
||||
import com.bitnix.dirtcryptostore.model.BlockchainPaymentMatch;
|
||||
import com.bitnix.dirtcryptostore.model.PendingInvoice;
|
||||
import com.bitnix.dirtcryptostore.model.Product;
|
||||
import com.bitnix.dirtcryptostore.model.WalletConfig;
|
||||
import com.bitnix.dirtcryptostore.util.EffectUtil;
|
||||
import com.bitnix.dirtcryptostore.util.MessageUtil;
|
||||
import com.bitnix.dirtcryptostore.util.TitleUtil;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
public class PaymentWatcher implements Runnable {
|
||||
|
||||
private final DirtCryptoStorePlugin plugin;
|
||||
private final BlockchainApi blockchainApi;
|
||||
|
||||
public PaymentWatcher(DirtCryptoStorePlugin plugin) {
|
||||
this.plugin = plugin;
|
||||
this.blockchainApi = new BlockchainApi(plugin);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
plugin.getInvoiceManager().cleanupExpiredInvoices();
|
||||
|
||||
for (PendingInvoice invoice : plugin.getInvoiceManager().getAllInvoices()) {
|
||||
if (!invoice.isMatchable()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (invoice.getStatus() == PendingInvoice.Status.PENDING && invoice.isExpired()) {
|
||||
plugin.getInvoiceManager().expireInvoice(invoice, "Expired by watcher.");
|
||||
notifyExpired(invoice);
|
||||
continue;
|
||||
}
|
||||
|
||||
BlockchainPaymentMatch match = blockchainApi.findPayment(invoice);
|
||||
if (!match.isFound()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (plugin.getInvoiceManager().isTxidAlreadyClaimed(match.getTxid(), invoice.getInvoiceId())) {
|
||||
plugin.getInvoiceManager().markUnderReview(
|
||||
invoice,
|
||||
match.getTxid(),
|
||||
match.getConfirmations(),
|
||||
"TXID already claimed by another invoice."
|
||||
);
|
||||
notifyUnderReview(invoice);
|
||||
continue;
|
||||
}
|
||||
|
||||
WalletConfig wallet = plugin.getConfigManager().getWallet(invoice.getWalletId());
|
||||
int requiredConfirmations = wallet == null
|
||||
? plugin.getConfig().getInt("settings.payment.confirmations-required", 2)
|
||||
: wallet.getMinConfirmations();
|
||||
|
||||
boolean firstDetection = invoice.getStatus() == PendingInvoice.Status.PENDING;
|
||||
|
||||
if (match.getConfirmations() >= requiredConfirmations) {
|
||||
fulfill(invoice, match, false);
|
||||
} else {
|
||||
plugin.getInvoiceManager().markDetected(
|
||||
invoice,
|
||||
match.getTxid(),
|
||||
match.getConfirmations(),
|
||||
"Payment detected, waiting for confirmations."
|
||||
);
|
||||
if (firstDetection) {
|
||||
notifyDetected(invoice);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void forceFulfillFromAdmin(PendingInvoice invoice, BlockchainPaymentMatch match) {
|
||||
fulfill(invoice, match, true);
|
||||
}
|
||||
|
||||
private void fulfill(PendingInvoice invoice, BlockchainPaymentMatch match, boolean manual) {
|
||||
if (invoice.isFulfilled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Product product = plugin.getConfigManager().getProduct(invoice.getProductId());
|
||||
WalletConfig wallet = plugin.getConfigManager().getWallet(invoice.getWalletId());
|
||||
|
||||
if (product == null) {
|
||||
plugin.getInvoiceManager().markUnderReview(
|
||||
invoice,
|
||||
match.getTxid(),
|
||||
match.getConfirmations(),
|
||||
"Product config missing during fulfillment."
|
||||
);
|
||||
notifyUnderReview(invoice);
|
||||
return;
|
||||
}
|
||||
|
||||
plugin.getInvoiceManager().markFulfilled(
|
||||
invoice,
|
||||
match.getTxid(),
|
||||
match.getConfirmations(),
|
||||
manual ? "Payment fulfilled manually by admin." : "Payment fulfilled automatically."
|
||||
);
|
||||
|
||||
Player player = Bukkit.getPlayer(invoice.getPlayerUuid());
|
||||
if (player != null) {
|
||||
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||
MessageUtil.send(plugin, player, "messages.payment-confirmed",
|
||||
"&aYour crypto payment for &f%product% &ahas been confirmed.",
|
||||
"%product%", product.getDisplayName());
|
||||
MessageUtil.send(plugin, player, "messages.payment-fulfilled",
|
||||
"&aYour purchase has been delivered.");
|
||||
EffectUtil.playEffect(plugin, player, "payment-confirmed");
|
||||
TitleUtil.sendTitle(plugin, player, "payment-confirmed");
|
||||
});
|
||||
}
|
||||
|
||||
boolean dispatchAsConsole = plugin.getConfig().getBoolean("settings.commands.dispatch-as-console", true);
|
||||
|
||||
for (String command : product.getCommands()) {
|
||||
String parsed = command
|
||||
.replace("%player%", invoice.getPlayerName())
|
||||
.replace("%uuid%", invoice.getPlayerUuid().toString())
|
||||
.replace("%product%", product.getId())
|
||||
.replace("%wallet%", wallet == null ? invoice.getWalletId() : wallet.getId())
|
||||
.replace("%txid%", match.getTxid())
|
||||
.replace("%confirmations%", String.valueOf(match.getConfirmations()));
|
||||
|
||||
if (dispatchAsConsole) {
|
||||
Bukkit.getScheduler().runTask(plugin, () -> Bukkit.dispatchCommand(Bukkit.getConsoleSender(), parsed));
|
||||
}
|
||||
}
|
||||
|
||||
if (plugin.getConfig().getBoolean("settings.debug", false)) {
|
||||
plugin.getLogger().info((manual ? "[MANUAL] " : "[AUTO] ") +
|
||||
"Fulfilled invoice " + invoice.getInvoiceId()
|
||||
+ " txid=" + match.getTxid()
|
||||
+ " confirmations=" + match.getConfirmations());
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyExpired(PendingInvoice invoice) {
|
||||
Player player = Bukkit.getPlayer(invoice.getPlayerUuid());
|
||||
if (player == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||
MessageUtil.send(plugin, player, "messages.invoice-expired", "&cThis payment request expired.");
|
||||
EffectUtil.playEffect(plugin, player, "expired");
|
||||
TitleUtil.sendTitle(plugin, player, "cancelled");
|
||||
});
|
||||
}
|
||||
|
||||
private void notifyDetected(PendingInvoice invoice) {
|
||||
Player player = Bukkit.getPlayer(invoice.getPlayerUuid());
|
||||
Product product = plugin.getConfigManager().getProduct(invoice.getProductId());
|
||||
if (player == null || product == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||
MessageUtil.send(plugin, player, "messages.payment-detected",
|
||||
"&aPayment detected for &f%product%&a. Waiting for confirmations...",
|
||||
"%product%", product.getDisplayName());
|
||||
EffectUtil.playEffect(plugin, player, "payment-detected");
|
||||
TitleUtil.sendTitle(plugin, player, "payment-detected");
|
||||
});
|
||||
}
|
||||
|
||||
private void notifyUnderReview(PendingInvoice invoice) {
|
||||
Player player = Bukkit.getPlayer(invoice.getPlayerUuid());
|
||||
if (player == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||
MessageUtil.send(plugin, player, "messages.payment-under-review",
|
||||
"&eA matching payment was found but needs review.");
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package com.bitnix.dirtcryptostore.manager;
|
||||
|
||||
import com.bitnix.dirtcryptostore.DirtCryptoStorePlugin;
|
||||
import com.bitnix.dirtcryptostore.model.Product;
|
||||
import com.bitnix.dirtcryptostore.util.HttpUtil;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
|
||||
public class PricingManager {
|
||||
|
||||
private final DirtCryptoStorePlugin plugin;
|
||||
|
||||
private volatile double cachedBtcUsdPrice = -1.0D;
|
||||
private volatile long lastFetchTime = 0L;
|
||||
|
||||
public PricingManager(DirtCryptoStorePlugin plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
public long resolveBaseSats(Product product) {
|
||||
String mode = plugin.getConfig().getString("settings.pricing.mode", "BASE_SATS");
|
||||
|
||||
if (!"LIVE_USD".equalsIgnoreCase(mode)) {
|
||||
return product.getBaseSats();
|
||||
}
|
||||
|
||||
Double btcUsdPrice = getLiveBtcUsdPrice();
|
||||
if (btcUsdPrice == null || btcUsdPrice <= 0.0D) {
|
||||
boolean fallback = plugin.getConfig().getBoolean("settings.pricing.fallback-to-base-sats", true);
|
||||
return fallback ? product.getBaseSats() : -1L;
|
||||
}
|
||||
|
||||
double usdPrice = product.getPriceUsd();
|
||||
if (usdPrice <= 0.0D) {
|
||||
return product.getBaseSats();
|
||||
}
|
||||
|
||||
double btcAmount = usdPrice / btcUsdPrice;
|
||||
double sats = btcAmount * 100_000_000.0D;
|
||||
|
||||
boolean roundUp = plugin.getConfig().getBoolean("settings.pricing.round-up-to-sat", true);
|
||||
long resolved = roundUp ? (long) Math.ceil(sats) : Math.round(sats);
|
||||
|
||||
if (resolved <= 0L) {
|
||||
return -1L;
|
||||
}
|
||||
|
||||
return resolved;
|
||||
}
|
||||
|
||||
public Double getLiveBtcUsdPrice() {
|
||||
long cacheSeconds = plugin.getConfig().getLong("settings.pricing.cache-seconds", 60L);
|
||||
long now = System.currentTimeMillis();
|
||||
|
||||
if (cachedBtcUsdPrice > 0.0D && (now - lastFetchTime) < (cacheSeconds * 1000L)) {
|
||||
return cachedBtcUsdPrice;
|
||||
}
|
||||
|
||||
synchronized (this) {
|
||||
if (cachedBtcUsdPrice > 0.0D && (now - lastFetchTime) < (cacheSeconds * 1000L)) {
|
||||
return cachedBtcUsdPrice;
|
||||
}
|
||||
|
||||
try {
|
||||
String provider = plugin.getConfig().getString("settings.pricing.live-rate-provider", "COINGECKO");
|
||||
if (!"COINGECKO".equalsIgnoreCase(provider)) {
|
||||
return cachedBtcUsdPrice > 0.0D ? cachedBtcUsdPrice : null;
|
||||
}
|
||||
|
||||
int timeout = plugin.getConfig().getInt("settings.blockchain.request-timeout-seconds", 10);
|
||||
String userAgent = plugin.getConfig().getString("settings.blockchain.user-agent", "DirtCryptoStore/1.0.0");
|
||||
String url = plugin.getConfig().getString(
|
||||
"settings.pricing.coingecko-url",
|
||||
"https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd"
|
||||
);
|
||||
|
||||
String json = HttpUtil.get(url, timeout, userAgent);
|
||||
JsonObject root = JsonParser.parseString(json).getAsJsonObject();
|
||||
JsonObject bitcoin = root.getAsJsonObject("bitcoin");
|
||||
if (bitcoin == null || !bitcoin.has("usd")) {
|
||||
return cachedBtcUsdPrice > 0.0D ? cachedBtcUsdPrice : null;
|
||||
}
|
||||
|
||||
double price = bitcoin.get("usd").getAsDouble();
|
||||
if (price > 0.0D) {
|
||||
cachedBtcUsdPrice = price;
|
||||
lastFetchTime = now;
|
||||
|
||||
if (plugin.getConfig().getBoolean("settings.debug", false)) {
|
||||
plugin.getLogger().info("Updated live BTC/USD price: " + price);
|
||||
}
|
||||
|
||||
return price;
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
if (plugin.getConfig().getBoolean("settings.debug", false)) {
|
||||
plugin.getLogger().warning("Failed to fetch live BTC price: " + ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return cachedBtcUsdPrice > 0.0D ? cachedBtcUsdPrice : null;
|
||||
}
|
||||
}
|
||||
|
||||
public void clearCache() {
|
||||
cachedBtcUsdPrice = -1.0D;
|
||||
lastFetchTime = 0L;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package com.bitnix.dirtcryptostore.manager;
|
||||
|
||||
import com.bitnix.dirtcryptostore.DirtCryptoStorePlugin;
|
||||
import com.bitnix.dirtcryptostore.model.PendingInvoice;
|
||||
import com.bitnix.dirtcryptostore.util.ColorUtil;
|
||||
import com.bitnix.dirtcryptostore.util.QrMapUtil;
|
||||
import com.bitnix.dirtcryptostore.util.QrPlaceholderUtil;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
public class QrMapManager {
|
||||
|
||||
private final DirtCryptoStorePlugin plugin;
|
||||
private final Map<UUID, ItemStack> activeQrMaps = new HashMap<>();
|
||||
|
||||
public QrMapManager(DirtCryptoStorePlugin plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
public ItemStack createQrMap(Player player, PendingInvoice invoice) {
|
||||
String bitcoinUri = QrPlaceholderUtil.buildBitcoinUri(invoice);
|
||||
|
||||
return QrMapUtil.createQrMapItem(
|
||||
plugin,
|
||||
player,
|
||||
bitcoinUri,
|
||||
ColorUtil.color("&aScan QR To Pay"),
|
||||
java.util.List.of(
|
||||
ColorUtil.color("&7Invoice: &f" + invoice.getInvoiceId()),
|
||||
ColorUtil.color("&7Amount: &f" + com.bitnix.dirtcryptostore.util.TimeUtil.formatBtcAmount(invoice.getAmountSats())),
|
||||
ColorUtil.color("&7Address: &f" + invoice.getWalletAddress()),
|
||||
ColorUtil.color("&7Use /cryptostore to reopen controls.")
|
||||
),
|
||||
plugin.getConfig().getString("settings.qr.fallback-material-if-map-fails", "PAPER")
|
||||
);
|
||||
}
|
||||
|
||||
public void giveToOffhand(Player player, PendingInvoice invoice) {
|
||||
ItemStack qrMap = createQrMap(player, invoice);
|
||||
activeQrMaps.put(player.getUniqueId(), qrMap);
|
||||
player.getInventory().setItemInOffHand(qrMap);
|
||||
player.updateInventory();
|
||||
}
|
||||
|
||||
public void clear(Player player) {
|
||||
activeQrMaps.remove(player.getUniqueId());
|
||||
}
|
||||
|
||||
public ItemStack getActiveMap(UUID uuid) {
|
||||
return activeQrMaps.get(uuid);
|
||||
}
|
||||
|
||||
public boolean hasActiveMap(UUID uuid) {
|
||||
return activeQrMaps.containsKey(uuid);
|
||||
}
|
||||
|
||||
public void clearAll() {
|
||||
activeQrMaps.clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.bitnix.dirtcryptostore.model;
|
||||
|
||||
public class BlockchainPaymentMatch {
|
||||
|
||||
private final boolean found;
|
||||
private final String txid;
|
||||
private final int confirmations;
|
||||
private final int blockHeight;
|
||||
|
||||
public BlockchainPaymentMatch(boolean found, String txid, int confirmations, int blockHeight) {
|
||||
this.found = found;
|
||||
this.txid = txid;
|
||||
this.confirmations = confirmations;
|
||||
this.blockHeight = blockHeight;
|
||||
}
|
||||
|
||||
public boolean isFound() {
|
||||
return found;
|
||||
}
|
||||
|
||||
public String getTxid() {
|
||||
return txid;
|
||||
}
|
||||
|
||||
public int getConfirmations() {
|
||||
return confirmations;
|
||||
}
|
||||
|
||||
public int getBlockHeight() {
|
||||
return blockHeight;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
package com.bitnix.dirtcryptostore.model;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public class PendingInvoice {
|
||||
|
||||
public enum Status {
|
||||
PENDING,
|
||||
DETECTED,
|
||||
FULFILLED,
|
||||
EXPIRED,
|
||||
CANCELLED,
|
||||
UNDER_REVIEW
|
||||
}
|
||||
|
||||
private final String invoiceId;
|
||||
private final UUID playerUuid;
|
||||
private final String playerName;
|
||||
private final String productId;
|
||||
private final String walletId;
|
||||
private final String walletAddress;
|
||||
private final long amountSats;
|
||||
private final long createdAt;
|
||||
private final long expiresAt;
|
||||
|
||||
private Status status;
|
||||
private String txid;
|
||||
private int confirmations;
|
||||
private boolean fulfilled;
|
||||
private long matchedAt;
|
||||
private long fulfilledAt;
|
||||
private String notes;
|
||||
|
||||
public PendingInvoice(String invoiceId,
|
||||
UUID playerUuid,
|
||||
String playerName,
|
||||
String productId,
|
||||
String walletId,
|
||||
String walletAddress,
|
||||
long amountSats,
|
||||
long createdAt,
|
||||
long expiresAt,
|
||||
Status status,
|
||||
String txid,
|
||||
int confirmations,
|
||||
boolean fulfilled,
|
||||
long matchedAt,
|
||||
long fulfilledAt,
|
||||
String notes) {
|
||||
this.invoiceId = invoiceId;
|
||||
this.playerUuid = playerUuid;
|
||||
this.playerName = playerName;
|
||||
this.productId = productId;
|
||||
this.walletId = walletId;
|
||||
this.walletAddress = walletAddress;
|
||||
this.amountSats = amountSats;
|
||||
this.createdAt = createdAt;
|
||||
this.expiresAt = expiresAt;
|
||||
this.status = status == null ? Status.PENDING : status;
|
||||
this.txid = txid == null ? "" : txid;
|
||||
this.confirmations = confirmations;
|
||||
this.fulfilled = fulfilled;
|
||||
this.matchedAt = matchedAt;
|
||||
this.fulfilledAt = fulfilledAt;
|
||||
this.notes = notes == null ? "" : notes;
|
||||
}
|
||||
|
||||
public String getInvoiceId() {
|
||||
return invoiceId;
|
||||
}
|
||||
|
||||
public UUID getPlayerUuid() {
|
||||
return playerUuid;
|
||||
}
|
||||
|
||||
public String getPlayerName() {
|
||||
return playerName;
|
||||
}
|
||||
|
||||
public String getProductId() {
|
||||
return productId;
|
||||
}
|
||||
|
||||
public String getWalletId() {
|
||||
return walletId;
|
||||
}
|
||||
|
||||
public String getWalletAddress() {
|
||||
return walletAddress;
|
||||
}
|
||||
|
||||
public long getAmountSats() {
|
||||
return amountSats;
|
||||
}
|
||||
|
||||
public long getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public long getExpiresAt() {
|
||||
return expiresAt;
|
||||
}
|
||||
|
||||
public Status getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public String getTxid() {
|
||||
return txid;
|
||||
}
|
||||
|
||||
public int getConfirmations() {
|
||||
return confirmations;
|
||||
}
|
||||
|
||||
public boolean isFulfilled() {
|
||||
return fulfilled;
|
||||
}
|
||||
|
||||
public long getMatchedAt() {
|
||||
return matchedAt;
|
||||
}
|
||||
|
||||
public long getFulfilledAt() {
|
||||
return fulfilledAt;
|
||||
}
|
||||
|
||||
public String getNotes() {
|
||||
return notes;
|
||||
}
|
||||
|
||||
public void setStatus(Status status) {
|
||||
this.status = status == null ? Status.PENDING : status;
|
||||
}
|
||||
|
||||
public void setTxid(String txid) {
|
||||
this.txid = txid == null ? "" : txid;
|
||||
}
|
||||
|
||||
public void setConfirmations(int confirmations) {
|
||||
this.confirmations = confirmations;
|
||||
}
|
||||
|
||||
public void setFulfilled(boolean fulfilled) {
|
||||
this.fulfilled = fulfilled;
|
||||
}
|
||||
|
||||
public void setMatchedAt(long matchedAt) {
|
||||
this.matchedAt = matchedAt;
|
||||
}
|
||||
|
||||
public void setFulfilledAt(long fulfilledAt) {
|
||||
this.fulfilledAt = fulfilledAt;
|
||||
}
|
||||
|
||||
public void setNotes(String notes) {
|
||||
this.notes = notes == null ? "" : notes;
|
||||
}
|
||||
|
||||
public boolean isExpired() {
|
||||
return System.currentTimeMillis() > expiresAt;
|
||||
}
|
||||
|
||||
public long getRemainingMillis() {
|
||||
return Math.max(0L, expiresAt - System.currentTimeMillis());
|
||||
}
|
||||
|
||||
public boolean isFinalState() {
|
||||
return status == Status.FULFILLED
|
||||
|| status == Status.EXPIRED
|
||||
|| status == Status.CANCELLED;
|
||||
}
|
||||
|
||||
public boolean isMatchable() {
|
||||
return status == Status.PENDING || status == Status.DETECTED;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package com.bitnix.dirtcryptostore.model;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class Product {
|
||||
|
||||
private final String id;
|
||||
private final boolean enabled;
|
||||
private final String displayName;
|
||||
private final String materialName;
|
||||
private final int customModelData;
|
||||
private final boolean glow;
|
||||
private final int slot;
|
||||
private final String wallet;
|
||||
private final double priceUsd;
|
||||
private final long baseSats;
|
||||
private final String permissionRequired;
|
||||
private final boolean purchasableOnce;
|
||||
private final List<String> commands;
|
||||
private final List<String> lore;
|
||||
|
||||
public Product(String id,
|
||||
boolean enabled,
|
||||
String displayName,
|
||||
String materialName,
|
||||
int customModelData,
|
||||
boolean glow,
|
||||
int slot,
|
||||
String wallet,
|
||||
double priceUsd,
|
||||
long baseSats,
|
||||
String permissionRequired,
|
||||
boolean purchasableOnce,
|
||||
List<String> commands,
|
||||
List<String> lore) {
|
||||
this.id = id;
|
||||
this.enabled = enabled;
|
||||
this.displayName = displayName;
|
||||
this.materialName = materialName;
|
||||
this.customModelData = customModelData;
|
||||
this.glow = glow;
|
||||
this.slot = slot;
|
||||
this.wallet = wallet;
|
||||
this.priceUsd = priceUsd;
|
||||
this.baseSats = baseSats;
|
||||
this.permissionRequired = permissionRequired == null ? "" : permissionRequired;
|
||||
this.purchasableOnce = purchasableOnce;
|
||||
this.commands = commands == null ? new ArrayList<>() : new ArrayList<>(commands);
|
||||
this.lore = lore == null ? new ArrayList<>() : new ArrayList<>(lore);
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
public String getMaterialName() {
|
||||
return materialName;
|
||||
}
|
||||
|
||||
public int getCustomModelData() {
|
||||
return customModelData;
|
||||
}
|
||||
|
||||
public boolean isGlow() {
|
||||
return glow;
|
||||
}
|
||||
|
||||
public int getSlot() {
|
||||
return slot;
|
||||
}
|
||||
|
||||
public String getWallet() {
|
||||
return wallet;
|
||||
}
|
||||
|
||||
public double getPriceUsd() {
|
||||
return priceUsd;
|
||||
}
|
||||
|
||||
public long getBaseSats() {
|
||||
return baseSats;
|
||||
}
|
||||
|
||||
public String getPermissionRequired() {
|
||||
return permissionRequired;
|
||||
}
|
||||
|
||||
public boolean isPurchasableOnce() {
|
||||
return purchasableOnce;
|
||||
}
|
||||
|
||||
public List<String> getCommands() {
|
||||
return new ArrayList<>(commands);
|
||||
}
|
||||
|
||||
public List<String> getLore() {
|
||||
return new ArrayList<>(lore);
|
||||
}
|
||||
|
||||
public String formatBtcAmount(long sats) {
|
||||
long whole = sats / 100_000_000L;
|
||||
long fractional = sats % 100_000_000L;
|
||||
return whole + "." + String.format("%08d", fractional) + " BTC";
|
||||
}
|
||||
|
||||
public String formatBitcoinUri(long sats, String address) {
|
||||
long whole = sats / 100_000_000L;
|
||||
long fractional = sats % 100_000_000L;
|
||||
String amount = whole + "." + String.format("%08d", fractional);
|
||||
return "bitcoin:" + address + "?amount=" + amount;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.bitnix.dirtcryptostore.model;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class WalletConfig {
|
||||
|
||||
private final String id;
|
||||
private final boolean enabled;
|
||||
private final String displayName;
|
||||
private final String coin;
|
||||
private final String address;
|
||||
private final String explorerType;
|
||||
private final int minConfirmations;
|
||||
private final List<String> allowProducts;
|
||||
|
||||
public WalletConfig(String id,
|
||||
boolean enabled,
|
||||
String displayName,
|
||||
String coin,
|
||||
String address,
|
||||
String explorerType,
|
||||
int minConfirmations,
|
||||
List<String> allowProducts) {
|
||||
this.id = id;
|
||||
this.enabled = enabled;
|
||||
this.displayName = displayName;
|
||||
this.coin = coin;
|
||||
this.address = address;
|
||||
this.explorerType = explorerType;
|
||||
this.minConfirmations = minConfirmations;
|
||||
this.allowProducts = allowProducts == null ? new ArrayList<>() : new ArrayList<>(allowProducts);
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
public String getCoin() {
|
||||
return coin;
|
||||
}
|
||||
|
||||
public String getAddress() {
|
||||
return address;
|
||||
}
|
||||
|
||||
public String getExplorerType() {
|
||||
return explorerType;
|
||||
}
|
||||
|
||||
public int getMinConfirmations() {
|
||||
return minConfirmations;
|
||||
}
|
||||
|
||||
public List<String> getAllowProducts() {
|
||||
return new ArrayList<>(allowProducts);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
package com.bitnix.dirtcryptostore.storage;
|
||||
|
||||
import com.bitnix.dirtcryptostore.DirtCryptoStorePlugin;
|
||||
import com.bitnix.dirtcryptostore.model.PendingInvoice;
|
||||
|
||||
import java.io.File;
|
||||
import java.sql.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.UUID;
|
||||
|
||||
public class SQLiteStorage {
|
||||
|
||||
private final DirtCryptoStorePlugin plugin;
|
||||
private final File databaseFile;
|
||||
|
||||
public SQLiteStorage(DirtCryptoStorePlugin plugin) {
|
||||
this.plugin = plugin;
|
||||
this.databaseFile = new File(plugin.getDataFolder(),
|
||||
plugin.getConfig().getString("settings.storage.sqlite-file", "dirtcryptostore.db"));
|
||||
}
|
||||
|
||||
public void init() {
|
||||
if (!plugin.getDataFolder().exists()) {
|
||||
plugin.getDataFolder().mkdirs();
|
||||
}
|
||||
|
||||
try (Connection connection = getConnection();
|
||||
Statement statement = connection.createStatement()) {
|
||||
|
||||
statement.executeUpdate("""
|
||||
CREATE TABLE IF NOT EXISTS invoices (
|
||||
invoice_id TEXT PRIMARY KEY,
|
||||
player_uuid TEXT NOT NULL,
|
||||
player_name TEXT NOT NULL,
|
||||
product_id TEXT NOT NULL,
|
||||
wallet_id TEXT NOT NULL,
|
||||
wallet_address TEXT NOT NULL,
|
||||
amount_sats INTEGER NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
expires_at INTEGER NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
txid TEXT NOT NULL,
|
||||
confirmations INTEGER NOT NULL,
|
||||
fulfilled INTEGER NOT NULL,
|
||||
matched_at INTEGER NOT NULL,
|
||||
fulfilled_at INTEGER NOT NULL,
|
||||
notes TEXT NOT NULL
|
||||
)
|
||||
""");
|
||||
|
||||
statement.executeUpdate("CREATE INDEX IF NOT EXISTS idx_invoices_player_uuid ON invoices(player_uuid)");
|
||||
statement.executeUpdate("CREATE INDEX IF NOT EXISTS idx_invoices_status ON invoices(status)");
|
||||
statement.executeUpdate("CREATE INDEX IF NOT EXISTS idx_invoices_txid ON invoices(txid)");
|
||||
statement.executeUpdate("CREATE INDEX IF NOT EXISTS idx_invoices_created_at ON invoices(created_at)");
|
||||
} catch (SQLException e) {
|
||||
plugin.getLogger().severe("Failed to initialize SQLite storage: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public List<PendingInvoice> loadAllInvoices() {
|
||||
List<PendingInvoice> invoices = new ArrayList<>();
|
||||
|
||||
try (Connection connection = getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement("SELECT * FROM invoices");
|
||||
ResultSet rs = statement.executeQuery()) {
|
||||
|
||||
while (rs.next()) {
|
||||
invoices.add(new PendingInvoice(
|
||||
rs.getString("invoice_id"),
|
||||
UUID.fromString(rs.getString("player_uuid")),
|
||||
rs.getString("player_name"),
|
||||
rs.getString("product_id"),
|
||||
rs.getString("wallet_id"),
|
||||
rs.getString("wallet_address"),
|
||||
rs.getLong("amount_sats"),
|
||||
rs.getLong("created_at"),
|
||||
rs.getLong("expires_at"),
|
||||
parseStatus(rs.getString("status")),
|
||||
rs.getString("txid"),
|
||||
rs.getInt("confirmations"),
|
||||
rs.getInt("fulfilled") == 1,
|
||||
rs.getLong("matched_at"),
|
||||
rs.getLong("fulfilled_at"),
|
||||
rs.getString("notes")
|
||||
));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().severe("Failed to load invoices from SQLite: " + e.getMessage());
|
||||
}
|
||||
|
||||
return invoices;
|
||||
}
|
||||
|
||||
public void saveInvoice(PendingInvoice invoice) {
|
||||
String sql = """
|
||||
INSERT INTO invoices (
|
||||
invoice_id, player_uuid, player_name, product_id, wallet_id, wallet_address,
|
||||
amount_sats, created_at, expires_at, status, txid, confirmations,
|
||||
fulfilled, matched_at, fulfilled_at, notes
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(invoice_id) DO UPDATE SET
|
||||
player_uuid=excluded.player_uuid,
|
||||
player_name=excluded.player_name,
|
||||
product_id=excluded.product_id,
|
||||
wallet_id=excluded.wallet_id,
|
||||
wallet_address=excluded.wallet_address,
|
||||
amount_sats=excluded.amount_sats,
|
||||
created_at=excluded.created_at,
|
||||
expires_at=excluded.expires_at,
|
||||
status=excluded.status,
|
||||
txid=excluded.txid,
|
||||
confirmations=excluded.confirmations,
|
||||
fulfilled=excluded.fulfilled,
|
||||
matched_at=excluded.matched_at,
|
||||
fulfilled_at=excluded.fulfilled_at,
|
||||
notes=excluded.notes
|
||||
""";
|
||||
|
||||
try (Connection connection = getConnection();
|
||||
PreparedStatement ps = connection.prepareStatement(sql)) {
|
||||
|
||||
ps.setString(1, invoice.getInvoiceId());
|
||||
ps.setString(2, invoice.getPlayerUuid().toString());
|
||||
ps.setString(3, invoice.getPlayerName());
|
||||
ps.setString(4, invoice.getProductId());
|
||||
ps.setString(5, invoice.getWalletId());
|
||||
ps.setString(6, invoice.getWalletAddress());
|
||||
ps.setLong(7, invoice.getAmountSats());
|
||||
ps.setLong(8, invoice.getCreatedAt());
|
||||
ps.setLong(9, invoice.getExpiresAt());
|
||||
ps.setString(10, invoice.getStatus().name());
|
||||
ps.setString(11, invoice.getTxid());
|
||||
ps.setInt(12, invoice.getConfirmations());
|
||||
ps.setInt(13, invoice.isFulfilled() ? 1 : 0);
|
||||
ps.setLong(14, invoice.getMatchedAt());
|
||||
ps.setLong(15, invoice.getFulfilledAt());
|
||||
ps.setString(16, invoice.getNotes());
|
||||
ps.executeUpdate();
|
||||
|
||||
} catch (SQLException e) {
|
||||
plugin.getLogger().severe("Failed to save invoice " + invoice.getInvoiceId() + " to SQLite: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public void deleteInvoice(String invoiceId) {
|
||||
try (Connection connection = getConnection();
|
||||
PreparedStatement ps = connection.prepareStatement("DELETE FROM invoices WHERE invoice_id = ?")) {
|
||||
ps.setString(1, invoiceId);
|
||||
ps.executeUpdate();
|
||||
} catch (SQLException e) {
|
||||
plugin.getLogger().severe("Failed to delete invoice " + invoiceId + " from SQLite: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public int pruneOldInvoices(long cutoffTimeMillis, List<String> statuses) {
|
||||
if (statuses == null || statuses.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
StringBuilder placeholders = new StringBuilder();
|
||||
for (int i = 0; i < statuses.size(); i++) {
|
||||
if (i > 0) placeholders.append(",");
|
||||
placeholders.append("?");
|
||||
}
|
||||
|
||||
String sql = "DELETE FROM invoices WHERE created_at < ? AND status IN (" + placeholders + ")";
|
||||
|
||||
try (Connection connection = getConnection();
|
||||
PreparedStatement ps = connection.prepareStatement(sql)) {
|
||||
|
||||
ps.setLong(1, cutoffTimeMillis);
|
||||
for (int i = 0; i < statuses.size(); i++) {
|
||||
ps.setString(i + 2, statuses.get(i).toUpperCase(Locale.ROOT));
|
||||
}
|
||||
|
||||
return ps.executeUpdate();
|
||||
} catch (SQLException e) {
|
||||
plugin.getLogger().severe("Failed to prune old invoices from SQLite: " + e.getMessage());
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private Connection getConnection() throws SQLException {
|
||||
return DriverManager.getConnection("jdbc:sqlite:" + databaseFile.getAbsolutePath());
|
||||
}
|
||||
|
||||
private PendingInvoice.Status parseStatus(String raw) {
|
||||
try {
|
||||
return PendingInvoice.Status.valueOf(raw.toUpperCase(Locale.ROOT));
|
||||
} catch (Exception ignored) {
|
||||
return PendingInvoice.Status.PENDING;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.bitnix.dirtcryptostore.util;
|
||||
|
||||
import org.bukkit.ChatColor;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public final class ColorUtil {
|
||||
|
||||
private ColorUtil() {
|
||||
}
|
||||
|
||||
public static String color(String text) {
|
||||
if (text == null) {
|
||||
return "";
|
||||
}
|
||||
return ChatColor.translateAlternateColorCodes('&', text);
|
||||
}
|
||||
|
||||
public static List<String> color(List<String> lines) {
|
||||
List<String> result = new ArrayList<>();
|
||||
if (lines == null) {
|
||||
return result;
|
||||
}
|
||||
|
||||
for (String line : lines) {
|
||||
result.add(color(line));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.bitnix.dirtcryptostore.util;
|
||||
|
||||
import com.bitnix.dirtcryptostore.DirtCryptoStorePlugin;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Particle;
|
||||
import org.bukkit.configuration.ConfigurationSection;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
public final class EffectUtil {
|
||||
|
||||
private EffectUtil() {
|
||||
}
|
||||
|
||||
public static void playEffect(DirtCryptoStorePlugin plugin, Player player, String key) {
|
||||
if (!plugin.getConfig().getBoolean("effects.enabled", true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
ConfigurationSection section = plugin.getConfig().getConfigurationSection("effects." + key);
|
||||
if (section == null || !section.getBoolean("enabled", false)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Particle particle = Particle.valueOf(section.getString("type", "VILLAGER_HAPPY"));
|
||||
int count = section.getInt("count", 10);
|
||||
double offsetX = section.getDouble("offset-x", 0.3D);
|
||||
double offsetY = section.getDouble("offset-y", 0.6D);
|
||||
double offsetZ = section.getDouble("offset-z", 0.3D);
|
||||
double extra = section.getDouble("extra", 0.0D);
|
||||
|
||||
Location location = player.getLocation().clone().add(0, 1, 0);
|
||||
player.getWorld().spawnParticle(particle, location, count, offsetX, offsetY, offsetZ, extra);
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.bitnix.dirtcryptostore.util;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URI;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
public final class HttpUtil {
|
||||
|
||||
private HttpUtil() {
|
||||
}
|
||||
|
||||
public static String get(String url, int timeoutSeconds, String userAgent) throws Exception {
|
||||
HttpURLConnection connection = (HttpURLConnection) URI.create(url).toURL().openConnection();
|
||||
connection.setRequestMethod("GET");
|
||||
connection.setConnectTimeout(timeoutSeconds * 1000);
|
||||
connection.setReadTimeout(timeoutSeconds * 1000);
|
||||
connection.setRequestProperty("User-Agent", userAgent);
|
||||
connection.setRequestProperty("Accept", "application/json");
|
||||
|
||||
int code = connection.getResponseCode();
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(
|
||||
code >= 200 && code < 300 ? connection.getInputStream() : connection.getErrorStream(),
|
||||
StandardCharsets.UTF_8
|
||||
));
|
||||
|
||||
StringBuilder response = new StringBuilder();
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
response.append(line);
|
||||
}
|
||||
reader.close();
|
||||
connection.disconnect();
|
||||
|
||||
if (code < 200 || code >= 300) {
|
||||
throw new IllegalStateException("HTTP " + code + " response: " + response);
|
||||
}
|
||||
|
||||
return response.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.bitnix.dirtcryptostore.util;
|
||||
|
||||
import com.bitnix.dirtcryptostore.DirtCryptoStorePlugin;
|
||||
import org.bukkit.command.CommandSender;
|
||||
|
||||
public final class MessageUtil {
|
||||
|
||||
private MessageUtil() {
|
||||
}
|
||||
|
||||
public static void send(DirtCryptoStorePlugin plugin, CommandSender sender, String path, String fallback) {
|
||||
String prefix = plugin.getConfig().getString("messages.prefix", "&8[&6DirtCryptoStore&8] ");
|
||||
String message = plugin.getConfig().getString(path, fallback);
|
||||
sender.sendMessage(ColorUtil.color(prefix + message));
|
||||
}
|
||||
|
||||
public static void send(DirtCryptoStorePlugin plugin, CommandSender sender, String path, String fallback, String... replacements) {
|
||||
String prefix = plugin.getConfig().getString("messages.prefix", "&8[&6DirtCryptoStore&8] ");
|
||||
String message = plugin.getConfig().getString(path, fallback);
|
||||
|
||||
if (replacements != null) {
|
||||
for (int i = 0; i + 1 < replacements.length; i += 2) {
|
||||
String key = replacements[i];
|
||||
String value = replacements[i + 1];
|
||||
message = message.replace(key, ColorUtil.color(value));
|
||||
}
|
||||
}
|
||||
|
||||
sender.sendMessage(ColorUtil.color(prefix + message));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package com.bitnix.dirtcryptostore.util;
|
||||
|
||||
import com.bitnix.dirtcryptostore.DirtCryptoStorePlugin;
|
||||
import com.google.zxing.BarcodeFormat;
|
||||
import com.google.zxing.MultiFormatWriter;
|
||||
import com.google.zxing.common.BitMatrix;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.MapMeta;
|
||||
import org.bukkit.map.*;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.List;
|
||||
|
||||
public final class QrMapUtil {
|
||||
|
||||
private QrMapUtil() {
|
||||
}
|
||||
|
||||
public static ItemStack createQrMapItem(DirtCryptoStorePlugin plugin,
|
||||
org.bukkit.entity.Player player,
|
||||
String content,
|
||||
String displayName,
|
||||
List<String> lore,
|
||||
String fallbackMaterialName) {
|
||||
try {
|
||||
World world = player.getWorld();
|
||||
MapView mapView = Bukkit.createMap(world);
|
||||
mapView.getRenderers().clear();
|
||||
mapView.setTrackingPosition(false);
|
||||
mapView.setUnlimitedTracking(false);
|
||||
mapView.setScale(MapView.Scale.CLOSE);
|
||||
|
||||
BufferedImage image = generateQr(content, 128, 128);
|
||||
mapView.addRenderer(new ImageRenderer(image));
|
||||
|
||||
ItemStack mapItem = new ItemStack(Material.FILLED_MAP);
|
||||
MapMeta meta = (MapMeta) mapItem.getItemMeta();
|
||||
if (meta != null) {
|
||||
meta.setDisplayName(displayName);
|
||||
meta.setLore(lore);
|
||||
meta.setMapView(mapView);
|
||||
mapItem.setItemMeta(meta);
|
||||
}
|
||||
|
||||
return mapItem;
|
||||
} catch (Exception ex) {
|
||||
if (plugin.getConfig().getBoolean("settings.debug", false)) {
|
||||
plugin.getLogger().warning("Failed to generate QR map: " + ex.getMessage());
|
||||
}
|
||||
|
||||
Material fallback = Material.matchMaterial(fallbackMaterialName);
|
||||
if (fallback == null) {
|
||||
fallback = Material.PAPER;
|
||||
}
|
||||
|
||||
ItemStack item = new ItemStack(fallback);
|
||||
org.bukkit.inventory.meta.ItemMeta meta = item.getItemMeta();
|
||||
if (meta != null) {
|
||||
meta.setDisplayName(displayName);
|
||||
meta.setLore(lore);
|
||||
item.setItemMeta(meta);
|
||||
}
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
private static BufferedImage generateQr(String content, int width, int height) throws Exception {
|
||||
BitMatrix matrix = new MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, width, height);
|
||||
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
|
||||
|
||||
for (int x = 0; x < width; x++) {
|
||||
for (int y = 0; y < height; y++) {
|
||||
image.setRGB(x, y, matrix.get(x, y) ? 0x000000 : 0xFFFFFF);
|
||||
}
|
||||
}
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
private static final class ImageRenderer extends MapRenderer {
|
||||
|
||||
private final BufferedImage image;
|
||||
private boolean rendered = false;
|
||||
|
||||
private ImageRenderer(BufferedImage image) {
|
||||
super(true);
|
||||
this.image = image;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(MapView mapView, MapCanvas mapCanvas, org.bukkit.entity.Player player) {
|
||||
if (rendered) {
|
||||
return;
|
||||
}
|
||||
|
||||
mapCanvas.drawImage(0, 0, image);
|
||||
rendered = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.bitnix.dirtcryptostore.util;
|
||||
|
||||
import com.bitnix.dirtcryptostore.model.PendingInvoice;
|
||||
|
||||
public final class QrPlaceholderUtil {
|
||||
|
||||
private QrPlaceholderUtil() {
|
||||
}
|
||||
|
||||
public static String buildBitcoinUri(PendingInvoice invoice) {
|
||||
return "bitcoin:" + invoice.getWalletAddress() + "?amount=" + TimeUtil.formatBtcRaw(invoice.getAmountSats());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package com.bitnix.dirtcryptostore.util;
|
||||
|
||||
public final class SimpleJson {
|
||||
|
||||
private SimpleJson() {
|
||||
}
|
||||
|
||||
public static String findString(String json, String key) {
|
||||
String pattern = "\"" + key + "\":";
|
||||
int start = json.indexOf(pattern);
|
||||
if (start == -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int firstQuote = json.indexOf('"', start + pattern.length());
|
||||
if (firstQuote == -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int secondQuote = json.indexOf('"', firstQuote + 1);
|
||||
if (secondQuote == -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return json.substring(firstQuote + 1, secondQuote);
|
||||
}
|
||||
|
||||
public static Integer findInt(String json, String key) {
|
||||
String pattern = "\"" + key + "\":";
|
||||
int start = json.indexOf(pattern);
|
||||
if (start == -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int index = start + pattern.length();
|
||||
while (index < json.length() && Character.isWhitespace(json.charAt(index))) {
|
||||
index++;
|
||||
}
|
||||
|
||||
int end = index;
|
||||
while (end < json.length() && (Character.isDigit(json.charAt(end)) || json.charAt(end) == '-')) {
|
||||
end++;
|
||||
}
|
||||
|
||||
if (end == index) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return Integer.parseInt(json.substring(index, end));
|
||||
} catch (NumberFormatException ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static Long findLong(String json, String key) {
|
||||
String pattern = "\"" + key + "\":";
|
||||
int start = json.indexOf(pattern);
|
||||
if (start == -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int index = start + pattern.length();
|
||||
while (index < json.length() && Character.isWhitespace(json.charAt(index))) {
|
||||
index++;
|
||||
}
|
||||
|
||||
int end = index;
|
||||
while (end < json.length() && (Character.isDigit(json.charAt(end)) || json.charAt(end) == '-')) {
|
||||
end++;
|
||||
}
|
||||
|
||||
if (end == index) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return Long.parseLong(json.substring(index, end));
|
||||
} catch (NumberFormatException ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.bitnix.dirtcryptostore.util;
|
||||
|
||||
public final class TimeUtil {
|
||||
|
||||
private TimeUtil() {
|
||||
}
|
||||
|
||||
public static String formatRemaining(long millis) {
|
||||
long totalSeconds = Math.max(0L, millis / 1000L);
|
||||
long minutes = totalSeconds / 60L;
|
||||
long seconds = totalSeconds % 60L;
|
||||
return minutes + "m " + seconds + "s";
|
||||
}
|
||||
|
||||
public static String formatBtcAmount(long sats) {
|
||||
long whole = sats / 100_000_000L;
|
||||
long fractional = sats % 100_000_000L;
|
||||
return whole + "." + String.format("%08d", fractional) + " BTC";
|
||||
}
|
||||
|
||||
public static String formatBtcRaw(long sats) {
|
||||
long whole = sats / 100_000_000L;
|
||||
long fractional = sats % 100_000_000L;
|
||||
return whole + "." + String.format("%08d", fractional);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.bitnix.dirtcryptostore.util;
|
||||
|
||||
import com.bitnix.dirtcryptostore.DirtCryptoStorePlugin;
|
||||
import org.bukkit.configuration.ConfigurationSection;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
public final class TitleUtil {
|
||||
|
||||
private TitleUtil() {
|
||||
}
|
||||
|
||||
public static void sendTitle(DirtCryptoStorePlugin plugin, Player player, String key) {
|
||||
if (!plugin.getConfig().getBoolean("titles.enabled", true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
ConfigurationSection section = plugin.getConfig().getConfigurationSection("titles." + key);
|
||||
if (section == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
String title = ColorUtil.color(section.getString("title", ""));
|
||||
String subtitle = ColorUtil.color(section.getString("subtitle", ""));
|
||||
int fadeIn = section.getInt("fade-in", 10);
|
||||
int stay = section.getInt("stay", 50);
|
||||
int fadeOut = section.getInt("fade-out", 10);
|
||||
|
||||
player.sendTitle(title, subtitle, fadeIn, stay, fadeOut);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,472 @@
|
||||
settings:
|
||||
debug: false
|
||||
|
||||
payment:
|
||||
poll-seconds: 30
|
||||
invoice-expire-minutes: 30
|
||||
confirmations-required: 2
|
||||
max-pending-per-player: 1
|
||||
allow-cancel: true
|
||||
auto-close-paid-invoice-seconds: 5
|
||||
mark-expired-on-open: true
|
||||
strict-exact-amount-match: true
|
||||
allow-late-match-if-unclaimed: true
|
||||
late-match-grace-minutes: 120
|
||||
|
||||
pricing:
|
||||
mode: "LIVE_USD"
|
||||
live-rate-provider: "COINGECKO"
|
||||
cache-seconds: 60
|
||||
fallback-to-base-sats: true
|
||||
round-up-to-sat: true
|
||||
coingecko-url: "https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd"
|
||||
|
||||
micro-amount:
|
||||
mode: "RANDOM_RANGE"
|
||||
min-extra-sats: 1
|
||||
max-extra-sats: 200
|
||||
sequential-start-sats: 1
|
||||
sequential-reset-when-server-restarts: false
|
||||
avoid-duplicate-active-amounts: true
|
||||
max-generation-attempts: 500
|
||||
|
||||
storage:
|
||||
type: "SQLITE"
|
||||
sqlite-file: "dirtcryptostore.db"
|
||||
prune-enabled: true
|
||||
prune-after-days: 30
|
||||
prune-statuses:
|
||||
- "FULFILLED"
|
||||
- "EXPIRED"
|
||||
- "CANCELLED"
|
||||
|
||||
qr:
|
||||
enabled: true
|
||||
map-title: "&8Crypto QR"
|
||||
include-bitcoin-uri-in-lore: true
|
||||
include-wallet-address-in-lore: true
|
||||
include-expiry-in-lore: true
|
||||
include-confirmations-in-lore: true
|
||||
show-qr-item-in-payment-gui: true
|
||||
fallback-material-if-map-fails: "PAPER"
|
||||
|
||||
blockchain:
|
||||
watcher-mode: "API"
|
||||
provider: "BLOCKCHAIN_INFO"
|
||||
request-timeout-seconds: 10
|
||||
user-agent: "DirtCryptoStore/1.0.0"
|
||||
wallet-cache-seconds: 20
|
||||
tx-cache-seconds: 20
|
||||
|
||||
commands:
|
||||
dispatch-as-console: true
|
||||
stop-on-command-failure: false
|
||||
log-dispatch: true
|
||||
|
||||
hooks:
|
||||
use-placeholderapi-if-present: false
|
||||
use-luckperms-context-if-present: false
|
||||
|
||||
wallets:
|
||||
default:
|
||||
enabled: true
|
||||
display-name: "&6Bitcoin Wallet"
|
||||
coin: "BTC"
|
||||
address: "PUT_YOUR_CAKE_WALLET_BTC_ADDRESS_HERE"
|
||||
explorer-type: "BTC"
|
||||
min-confirmations: 2
|
||||
allow-products: ["tip_jar", "gold_dust", "dirt_rich"]
|
||||
|
||||
messages:
|
||||
prefix: "&8[&6DirtCryptoStore&8] "
|
||||
|
||||
no-permission: "&cYou do not have permission."
|
||||
player-only: "&cOnly players can use this command."
|
||||
reloaded: "&aDirtCryptoStore reloaded."
|
||||
opening-store: "&aOpening crypto store..."
|
||||
admin-reload-usage: "&eUse: /cryptostore reload"
|
||||
|
||||
already-has-pending: "&cYou already have a pending payment."
|
||||
no-pending-payment: "&cYou do not have a pending payment."
|
||||
invoice-created: "&aInvoice created for &f%product%&a."
|
||||
invoice-expired: "&cThis payment request expired."
|
||||
invoice-cancelled: "&eYour pending payment was cancelled."
|
||||
invoice-paid-already: "&eThis invoice is already paid."
|
||||
invoice-view-only: "&eThis invoice can no longer be changed."
|
||||
pricing-unavailable: "&cLive BTC pricing is currently unavailable. Please try again in a moment."
|
||||
|
||||
payment-detected: "&aPayment detected for &f%product%&a. Waiting for confirmations..."
|
||||
payment-confirmed: "&aYour crypto payment for &f%product% &ahas been confirmed."
|
||||
payment-fulfilled: "&aYour purchase has been delivered."
|
||||
payment-under-review: "&cThis payment requires staff review."
|
||||
payment-not-found-yet: "&7No matching payment detected yet."
|
||||
payment-status-waiting: "&eStatus: WAITING"
|
||||
payment-status-detected: "&aStatus: DETECTED"
|
||||
payment-status-confirming: "&6Status: CONFIRMING"
|
||||
payment-status-paid: "&aStatus: PAID"
|
||||
payment-status-expired: "&cStatus: EXPIRED"
|
||||
payment-status-cancelled: "&cStatus: CANCELLED"
|
||||
|
||||
qr-expiry-line: "&7Expires in: &f%time%"
|
||||
confirm-line: "&7Required confirmations: &f%confirmations%"
|
||||
wallet-line: "&7Wallet: &f%wallet%"
|
||||
wallet-address-line: "&7Address: &f%address%"
|
||||
amount-line: "&7Amount: &f%amount%"
|
||||
amount-sats-line: "&7Amount (sats): &f%amount_sats%"
|
||||
product-line: "&7Product: &f%product%"
|
||||
txid-line: "&7TXID: &f%txid%"
|
||||
|
||||
gui-cancel-button: "&cCancel Payment"
|
||||
gui-back-button: "&eBack"
|
||||
gui-close-button: "&cClose"
|
||||
gui-refresh-button: "&bRefresh"
|
||||
gui-confirmed-button: "&aPaid"
|
||||
gui-expired-button: "&cExpired"
|
||||
gui-pending-button: "&ePending"
|
||||
|
||||
sounds:
|
||||
enabled: true
|
||||
|
||||
store-open:
|
||||
sound: "BLOCK_ENDER_CHEST_OPEN"
|
||||
volume: 1.0
|
||||
pitch: 1.0
|
||||
|
||||
click:
|
||||
sound: "UI_BUTTON_CLICK"
|
||||
volume: 1.0
|
||||
pitch: 1.0
|
||||
|
||||
invoice-created:
|
||||
sound: "ENTITY_EXPERIENCE_ORB_PICKUP"
|
||||
volume: 1.0
|
||||
pitch: 1.2
|
||||
|
||||
payment-detected:
|
||||
sound: "BLOCK_NOTE_BLOCK_PLING"
|
||||
volume: 1.0
|
||||
pitch: 1.5
|
||||
|
||||
payment-confirmed:
|
||||
sound: "ENTITY_PLAYER_LEVELUP"
|
||||
volume: 1.0
|
||||
pitch: 1.0
|
||||
|
||||
cancelled:
|
||||
sound: "ENTITY_VILLAGER_NO"
|
||||
volume: 1.0
|
||||
pitch: 1.0
|
||||
|
||||
expired:
|
||||
sound: "BLOCK_ANVIL_LAND"
|
||||
volume: 0.7
|
||||
pitch: 1.0
|
||||
|
||||
effects:
|
||||
enabled: true
|
||||
|
||||
store-open:
|
||||
enabled: false
|
||||
type: "VILLAGER_HAPPY"
|
||||
count: 10
|
||||
offset-x: 0.3
|
||||
offset-y: 0.6
|
||||
offset-z: 0.3
|
||||
extra: 0.0
|
||||
|
||||
invoice-created:
|
||||
enabled: true
|
||||
type: "TOTEM_OF_UNDYING"
|
||||
count: 12
|
||||
offset-x: 0.4
|
||||
offset-y: 0.8
|
||||
offset-z: 0.4
|
||||
extra: 0.0
|
||||
|
||||
payment-detected:
|
||||
enabled: true
|
||||
type: "COMPOSTER"
|
||||
count: 8
|
||||
offset-x: 0.3
|
||||
offset-y: 1.0
|
||||
offset-z: 0.3
|
||||
extra: 0.0
|
||||
|
||||
payment-confirmed:
|
||||
enabled: true
|
||||
type: "FIREWORK"
|
||||
count: 20
|
||||
offset-x: 0.5
|
||||
offset-y: 1.0
|
||||
offset-z: 0.5
|
||||
extra: 0.0
|
||||
|
||||
cancelled:
|
||||
enabled: false
|
||||
type: "SMOKE"
|
||||
count: 8
|
||||
offset-x: 0.2
|
||||
offset-y: 0.4
|
||||
offset-z: 0.2
|
||||
extra: 0.0
|
||||
|
||||
expired:
|
||||
enabled: false
|
||||
type: "SMOKE"
|
||||
count: 8
|
||||
offset-x: 0.2
|
||||
offset-y: 0.4
|
||||
offset-z: 0.2
|
||||
extra: 0.0
|
||||
|
||||
titles:
|
||||
enabled: true
|
||||
|
||||
invoice-created:
|
||||
title: "&6Crypto Invoice"
|
||||
subtitle: "&7Scan the QR and pay the exact amount"
|
||||
fade-in: 10
|
||||
stay: 50
|
||||
fade-out: 10
|
||||
|
||||
payment-detected:
|
||||
title: "&aPayment Detected"
|
||||
subtitle: "&7Waiting for confirmations"
|
||||
fade-in: 10
|
||||
stay: 40
|
||||
fade-out: 10
|
||||
|
||||
payment-confirmed:
|
||||
title: "&aPayment Confirmed"
|
||||
subtitle: "&fYour purchase has been delivered"
|
||||
fade-in: 10
|
||||
stay: 60
|
||||
fade-out: 10
|
||||
|
||||
cancelled:
|
||||
title: "&cPayment Cancelled"
|
||||
subtitle: "&7Your pending invoice was cancelled"
|
||||
fade-in: 10
|
||||
stay: 40
|
||||
fade-out: 10
|
||||
|
||||
gui:
|
||||
use-custom-model-data: false
|
||||
|
||||
store:
|
||||
title: "&8Crypto Store"
|
||||
size: 27
|
||||
open-sound: true
|
||||
|
||||
items:
|
||||
filler:
|
||||
enabled: true
|
||||
material: "GRAY_STAINED_GLASS_PANE"
|
||||
name: " "
|
||||
lore: []
|
||||
slots:
|
||||
- 0
|
||||
- 1
|
||||
- 2
|
||||
- 3
|
||||
- 4
|
||||
- 5
|
||||
- 6
|
||||
- 7
|
||||
- 8
|
||||
- 9
|
||||
- 17
|
||||
- 18
|
||||
- 19
|
||||
- 20
|
||||
- 21
|
||||
- 22
|
||||
- 23
|
||||
- 24
|
||||
- 25
|
||||
- 26
|
||||
|
||||
close:
|
||||
enabled: true
|
||||
material: "BARRIER"
|
||||
name: "&cClose"
|
||||
lore:
|
||||
- "&7Close the store."
|
||||
slot: 26
|
||||
|
||||
refresh:
|
||||
enabled: true
|
||||
material: "SUNFLOWER"
|
||||
name: "&eRefresh"
|
||||
lore:
|
||||
- "&7Refresh the store view."
|
||||
slot: 18
|
||||
|
||||
info:
|
||||
enabled: true
|
||||
material: "BOOK"
|
||||
name: "&6How Crypto Payments Work"
|
||||
lore:
|
||||
- "&7Click a product to create a payment invoice."
|
||||
- "&7You must send the exact amount shown."
|
||||
- "&7Your QR expires after &f30 minutes&7."
|
||||
- "&7You can cancel your payment from the payment menu."
|
||||
slot: 22
|
||||
|
||||
payment:
|
||||
title: "&8Crypto Payment"
|
||||
size: 27
|
||||
|
||||
items:
|
||||
filler:
|
||||
enabled: true
|
||||
material: "BLACK_STAINED_GLASS_PANE"
|
||||
name: " "
|
||||
lore: []
|
||||
slots:
|
||||
- 0
|
||||
- 1
|
||||
- 2
|
||||
- 3
|
||||
- 4
|
||||
- 5
|
||||
- 6
|
||||
- 7
|
||||
- 8
|
||||
- 9
|
||||
- 17
|
||||
- 18
|
||||
- 20
|
||||
- 24
|
||||
- 25
|
||||
- 26
|
||||
|
||||
product:
|
||||
enabled: true
|
||||
material: "CHEST"
|
||||
name: "&6Purchase Details"
|
||||
lore:
|
||||
- "&7Product: &f%product%"
|
||||
- "&7Price: &f%amount%"
|
||||
- "&7Wallet: &f%wallet%"
|
||||
- "&7Address: &f%address%"
|
||||
- "&7Status: &f%status%"
|
||||
- "&7Expires in: &f%time%"
|
||||
slot: 11
|
||||
|
||||
qr:
|
||||
enabled: true
|
||||
material: "FILLED_MAP"
|
||||
name: "&aScan QR To Pay"
|
||||
lore:
|
||||
- "&7Scan this QR with your wallet app."
|
||||
- "&7"
|
||||
- "&7Amount: &f%amount%"
|
||||
- "&7Address: &f%address%"
|
||||
- "&7"
|
||||
- "&7If your wallet does not scan maps well,"
|
||||
- "&7use the URI manually:"
|
||||
- "&f%bitcoin_uri%"
|
||||
slot: 13
|
||||
|
||||
status:
|
||||
enabled: true
|
||||
material: "CLOCK"
|
||||
name: "&ePayment Status"
|
||||
lore:
|
||||
- "&7Current: &f%status%"
|
||||
- "&7TXID: &f%txid%"
|
||||
- "&7Confirmations: &f%current_confirmations%/%confirmations%"
|
||||
- "&7Expires in: &f%time%"
|
||||
slot: 15
|
||||
|
||||
back:
|
||||
enabled: true
|
||||
material: "ARROW"
|
||||
name: "&eBack"
|
||||
lore:
|
||||
- "&7Return to the store."
|
||||
slot: 18
|
||||
|
||||
cancel:
|
||||
enabled: true
|
||||
material: "BARRIER"
|
||||
name: "&cCancel Payment"
|
||||
lore:
|
||||
- "&7Cancel this pending payment."
|
||||
slot: 22
|
||||
|
||||
refresh:
|
||||
enabled: true
|
||||
material: "SUNFLOWER"
|
||||
name: "&bRefresh Status"
|
||||
lore:
|
||||
- "&7Refresh this payment screen."
|
||||
slot: 26
|
||||
|
||||
products:
|
||||
tip_jar:
|
||||
enabled: true
|
||||
display-name: "&aTip Jar"
|
||||
material: "EMERALD"
|
||||
custom-model-data: 0
|
||||
glow: true
|
||||
slot: 11
|
||||
wallet: "default"
|
||||
price-usd: 3.49
|
||||
base-sats: 15000
|
||||
permission-required: ""
|
||||
purchasable-once: false
|
||||
commands:
|
||||
- "lp user %player% parent add tipjar"
|
||||
lore:
|
||||
- "&7Buy the &aTip Jar &7rank with crypto."
|
||||
- "&7Normal store price: &c$4.99"
|
||||
- "&7Crypto store price: &a$3.49"
|
||||
- "&7Wallet: &f%wallet%"
|
||||
- "&7Approx base amount: &f%amount%"
|
||||
- "&7Click to generate a live-priced invoice."
|
||||
|
||||
gold_dust:
|
||||
enabled: true
|
||||
display-name: "&bGold Dust"
|
||||
material: "GOLD_INGOT"
|
||||
custom-model-data: 0
|
||||
glow: true
|
||||
slot: 13
|
||||
wallet: "default"
|
||||
price-usd: 6.99
|
||||
base-sats: 27000
|
||||
permission-required: ""
|
||||
purchasable-once: false
|
||||
commands:
|
||||
- "lp user %player% parent add golddust"
|
||||
lore:
|
||||
- "&7Buy the &bGold Dust &7rank with crypto."
|
||||
- "&7Normal store price: &c$9.99"
|
||||
- "&7Crypto store price: &a$6.99"
|
||||
- "&7Wallet: &f%wallet%"
|
||||
- "&7Approx base amount: &f%amount%"
|
||||
- "&7Click to generate a live-priced invoice."
|
||||
|
||||
dirt_rich:
|
||||
enabled: true
|
||||
display-name: "&6Dirt Rich"
|
||||
material: "DIAMOND"
|
||||
custom-model-data: 0
|
||||
glow: false
|
||||
slot: 15
|
||||
wallet: "default"
|
||||
price-usd: 13.99
|
||||
base-sats: 9750
|
||||
permission-required: ""
|
||||
purchasable-once: false
|
||||
commands:
|
||||
- "lp user %player% parent add dirtrich"
|
||||
lore:
|
||||
- "&7Buy the &6Dirt Rich &7rank with crypto."
|
||||
- "&7Normal store price: &c$19.99"
|
||||
- "&7Crypto store price: &a$13.99"
|
||||
- "&7Wallet: &f%wallet%"
|
||||
- "&7Approx base amount: &f%amount%"
|
||||
- "&7Click to generate a live-priced invoice."
|
||||
@@ -0,0 +1,25 @@
|
||||
name: DirtCryptoStore
|
||||
version: 1.0.0
|
||||
main: com.bitnix.dirtcryptostore.DirtCryptoStorePlugin
|
||||
api-version: '1.21'
|
||||
description: Crypto GUI shop plugin for Paper
|
||||
author: bitnix
|
||||
|
||||
commands:
|
||||
cryptostore:
|
||||
description: Open the crypto store
|
||||
usage: /cryptostore [reload|status|review|forceconfirm|cancelinvoice]
|
||||
aliases: [cstore]
|
||||
|
||||
permissions:
|
||||
dirtcryptostore.use:
|
||||
description: Allows use of the crypto store
|
||||
default: true
|
||||
|
||||
dirtcryptostore.admin:
|
||||
description: Admin access to reload and manage the crypto store
|
||||
default: op
|
||||
|
||||
dirtcryptostore.bypass:
|
||||
description: Bypass restrictions if used later
|
||||
default: op
|
||||
Reference in New Issue
Block a user