first commit
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
# DirtCryptoStore
|
||||
|
||||
DirtCryptoStore is a Paper 1.21.x crypto shop plugin.
|
||||
|
||||
## Features
|
||||
|
||||
- GUI crypto store
|
||||
- Live BTC/USD pricing
|
||||
- Unique BTC invoice amounts per player
|
||||
- QR map generation
|
||||
- QR placed in player off-hand
|
||||
- SQLite storage
|
||||
- Expired invoice pruning
|
||||
- Auto payment watching
|
||||
- Admin review tools
|
||||
|
||||
## Requirements
|
||||
|
||||
- Paper 1.21.x
|
||||
- Java 21
|
||||
- Maven 3.8.7+
|
||||
|
||||
## Build
|
||||
|
||||
mvn clean package
|
||||
|
||||
Jar:
|
||||
target/DirtCryptoStore.jar
|
||||
|
||||
## Install
|
||||
|
||||
1. Put the jar in your server plugins folder
|
||||
2. Start the server
|
||||
3. Edit plugins/DirtCryptoStore/config.yml
|
||||
4. Restart the server or use /cryptostore reload
|
||||
|
||||
## Player Commands
|
||||
|
||||
/cryptostore
|
||||
Opens the store GUI
|
||||
|
||||
/cryptostore status
|
||||
Shows the player’s pending invoice status
|
||||
|
||||
## Admin Commands
|
||||
|
||||
/cryptostore reload
|
||||
Reloads plugin config and watcher
|
||||
|
||||
/cryptostore review
|
||||
Shows invoices marked UNDER_REVIEW
|
||||
|
||||
/cryptostore forceconfirm <invoiceId>
|
||||
Manually fulfills an invoice
|
||||
|
||||
/cryptostore cancelinvoice <invoiceId>
|
||||
Cancels an invoice
|
||||
|
||||
## Permissions
|
||||
|
||||
dirtcryptostore.use
|
||||
Default: true
|
||||
|
||||
dirtcryptostore.admin
|
||||
Default: op
|
||||
|
||||
dirtcryptostore.bypass
|
||||
Reserved for future use
|
||||
|
||||
## Payment Flow
|
||||
|
||||
1. Player runs /cryptostore
|
||||
2. Player clicks a product
|
||||
3. Plugin converts USD price to BTC
|
||||
4. Plugin adds a unique sat offset
|
||||
5. Invoice is created
|
||||
6. QR map is placed in off-hand
|
||||
7. Player pays exact BTC amount
|
||||
8. Plugin watches blockchain
|
||||
9. After confirmations, commands run
|
||||
|
||||
## Storage
|
||||
|
||||
SQLite database file:
|
||||
plugins/DirtCryptoStore/dirtcryptostore.db
|
||||
|
||||
## Pruning
|
||||
|
||||
Old invoices can be pruned with config options:
|
||||
- prune-enabled
|
||||
- prune-after-days
|
||||
- prune-statuses
|
||||
|
||||
## Notes
|
||||
|
||||
- BTC-focused
|
||||
- Live pricing uses CoinGecko
|
||||
- Payment watching uses blockchain.info
|
||||
- Large public deployments should later add provider fallback and audit logging
|
||||
@@ -0,0 +1,88 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://www.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>com.bitnix</groupId>
|
||||
<artifactId>DirtCryptoStore</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>DirtCryptoStore</name>
|
||||
<description>Crypto GUI shop plugin for Paper</description>
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<maven.compiler.release>21</maven.compiler.release>
|
||||
</properties>
|
||||
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>papermc-repo</id>
|
||||
<url>https://repo.papermc.io/repository/maven-public/</url>
|
||||
</repository>
|
||||
</repositories>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>io.papermc.paper</groupId>
|
||||
<artifactId>paper-api</artifactId>
|
||||
<version>1.21.1-R0.1-SNAPSHOT</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.google.zxing</groupId>
|
||||
<artifactId>core</artifactId>
|
||||
<version>3.5.3</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.google.zxing</groupId>
|
||||
<artifactId>javase</artifactId>
|
||||
<version>3.5.3</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.google.code.gson</groupId>
|
||||
<artifactId>gson</artifactId>
|
||||
<version>2.11.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.xerial</groupId>
|
||||
<artifactId>sqlite-jdbc</artifactId>
|
||||
<version>3.46.1.3</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<finalName>DirtCryptoStore</finalName>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.13.0</version>
|
||||
<configuration>
|
||||
<release>21</release>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<version>3.5.3</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>shade</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<createDependencyReducedPom>false</createDependencyReducedPom>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -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
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
@@ -0,0 +1,5 @@
|
||||
#Generated by Maven
|
||||
#Sat Jun 20 15:53:05 EDT 2026
|
||||
artifactId=DirtCryptoStore
|
||||
groupId=com.bitnix
|
||||
version=1.0.0
|
||||
@@ -0,0 +1,27 @@
|
||||
com/bitnix/dirtcryptostore/gui/StoreGui.class
|
||||
com/bitnix/dirtcryptostore/util/QrMapUtil.class
|
||||
com/bitnix/dirtcryptostore/util/EffectUtil.class
|
||||
com/bitnix/dirtcryptostore/util/TitleUtil.class
|
||||
com/bitnix/dirtcryptostore/model/WalletConfig.class
|
||||
com/bitnix/dirtcryptostore/command/CryptoStoreCommand.class
|
||||
com/bitnix/dirtcryptostore/manager/PricingManager.class
|
||||
com/bitnix/dirtcryptostore/util/SimpleJson.class
|
||||
com/bitnix/dirtcryptostore/util/HttpUtil.class
|
||||
com/bitnix/dirtcryptostore/model/Product.class
|
||||
com/bitnix/dirtcryptostore/gui/PaymentGui.class
|
||||
com/bitnix/dirtcryptostore/model/PendingInvoice$Status.class
|
||||
com/bitnix/dirtcryptostore/listener/StoreGuiListener.class
|
||||
com/bitnix/dirtcryptostore/storage/SQLiteStorage.class
|
||||
com/bitnix/dirtcryptostore/util/MessageUtil.class
|
||||
com/bitnix/dirtcryptostore/DirtCryptoStorePlugin.class
|
||||
com/bitnix/dirtcryptostore/manager/InvoiceManager.class
|
||||
com/bitnix/dirtcryptostore/manager/PaymentWatcher.class
|
||||
com/bitnix/dirtcryptostore/util/ColorUtil.class
|
||||
com/bitnix/dirtcryptostore/manager/QrMapManager.class
|
||||
com/bitnix/dirtcryptostore/model/BlockchainPaymentMatch.class
|
||||
com/bitnix/dirtcryptostore/manager/BlockchainApi.class
|
||||
com/bitnix/dirtcryptostore/util/TimeUtil.class
|
||||
com/bitnix/dirtcryptostore/util/QrMapUtil$ImageRenderer.class
|
||||
com/bitnix/dirtcryptostore/model/PendingInvoice.class
|
||||
com/bitnix/dirtcryptostore/manager/ConfigManager.class
|
||||
com/bitnix/dirtcryptostore/util/QrPlaceholderUtil.class
|
||||
@@ -0,0 +1,25 @@
|
||||
/home/bitnix/Desktop/DirtCryptoStore/src/main/java/com/bitnix/dirtcryptostore/DirtCryptoStorePlugin.java
|
||||
/home/bitnix/Desktop/DirtCryptoStore/src/main/java/com/bitnix/dirtcryptostore/command/CryptoStoreCommand.java
|
||||
/home/bitnix/Desktop/DirtCryptoStore/src/main/java/com/bitnix/dirtcryptostore/gui/PaymentGui.java
|
||||
/home/bitnix/Desktop/DirtCryptoStore/src/main/java/com/bitnix/dirtcryptostore/gui/StoreGui.java
|
||||
/home/bitnix/Desktop/DirtCryptoStore/src/main/java/com/bitnix/dirtcryptostore/listener/StoreGuiListener.java
|
||||
/home/bitnix/Desktop/DirtCryptoStore/src/main/java/com/bitnix/dirtcryptostore/manager/BlockchainApi.java
|
||||
/home/bitnix/Desktop/DirtCryptoStore/src/main/java/com/bitnix/dirtcryptostore/manager/ConfigManager.java
|
||||
/home/bitnix/Desktop/DirtCryptoStore/src/main/java/com/bitnix/dirtcryptostore/manager/InvoiceManager.java
|
||||
/home/bitnix/Desktop/DirtCryptoStore/src/main/java/com/bitnix/dirtcryptostore/manager/PaymentWatcher.java
|
||||
/home/bitnix/Desktop/DirtCryptoStore/src/main/java/com/bitnix/dirtcryptostore/manager/PricingManager.java
|
||||
/home/bitnix/Desktop/DirtCryptoStore/src/main/java/com/bitnix/dirtcryptostore/manager/QrMapManager.java
|
||||
/home/bitnix/Desktop/DirtCryptoStore/src/main/java/com/bitnix/dirtcryptostore/model/BlockchainPaymentMatch.java
|
||||
/home/bitnix/Desktop/DirtCryptoStore/src/main/java/com/bitnix/dirtcryptostore/model/PendingInvoice.java
|
||||
/home/bitnix/Desktop/DirtCryptoStore/src/main/java/com/bitnix/dirtcryptostore/model/Product.java
|
||||
/home/bitnix/Desktop/DirtCryptoStore/src/main/java/com/bitnix/dirtcryptostore/model/WalletConfig.java
|
||||
/home/bitnix/Desktop/DirtCryptoStore/src/main/java/com/bitnix/dirtcryptostore/storage/SQLiteStorage.java
|
||||
/home/bitnix/Desktop/DirtCryptoStore/src/main/java/com/bitnix/dirtcryptostore/util/ColorUtil.java
|
||||
/home/bitnix/Desktop/DirtCryptoStore/src/main/java/com/bitnix/dirtcryptostore/util/EffectUtil.java
|
||||
/home/bitnix/Desktop/DirtCryptoStore/src/main/java/com/bitnix/dirtcryptostore/util/HttpUtil.java
|
||||
/home/bitnix/Desktop/DirtCryptoStore/src/main/java/com/bitnix/dirtcryptostore/util/MessageUtil.java
|
||||
/home/bitnix/Desktop/DirtCryptoStore/src/main/java/com/bitnix/dirtcryptostore/util/QrMapUtil.java
|
||||
/home/bitnix/Desktop/DirtCryptoStore/src/main/java/com/bitnix/dirtcryptostore/util/QrPlaceholderUtil.java
|
||||
/home/bitnix/Desktop/DirtCryptoStore/src/main/java/com/bitnix/dirtcryptostore/util/SimpleJson.java
|
||||
/home/bitnix/Desktop/DirtCryptoStore/src/main/java/com/bitnix/dirtcryptostore/util/TimeUtil.java
|
||||
/home/bitnix/Desktop/DirtCryptoStore/src/main/java/com/bitnix/dirtcryptostore/util/TitleUtil.java
|
||||
Binary file not shown.
Reference in New Issue
Block a user