feat: Implement custom error classes, a Drizzle transaction utility, and update Discord.js ephemeral message flags.

This commit is contained in:
syntaxbullet
2025-12-15 22:14:17 +01:00
parent 3c81fd8396
commit 7e986fae5a
14 changed files with 112 additions and 114 deletions

View File

@@ -1,5 +1,5 @@
import { createCommand } from "@/lib/utils"; import { createCommand } from "@/lib/utils";
import { SlashCommandBuilder, EmbedBuilder } from "discord.js"; import { SlashCommandBuilder, EmbedBuilder, MessageFlags } from "discord.js";
import { economyService } from "@/modules/economy/economy.service"; import { economyService } from "@/modules/economy/economy.service";
import { createErrorEmbed, createWarningEmbed } from "@lib/embeds"; import { createErrorEmbed, createWarningEmbed } from "@lib/embeds";
@@ -25,12 +25,12 @@ export const daily = createCommand({
} catch (error: any) { } catch (error: any) {
if (error.message.includes("Daily already claimed")) { if (error.message.includes("Daily already claimed")) {
await interaction.reply({ embeds: [createWarningEmbed(error.message, "Cooldown")], ephemeral: true }); await interaction.reply({ embeds: [createWarningEmbed(error.message, "Cooldown")], flags: MessageFlags.Ephemeral });
return; return;
} }
console.error(error); console.error(error);
await interaction.reply({ embeds: [createErrorEmbed("An error occurred while claiming your daily reward.")], ephemeral: true }); await interaction.reply({ embeds: [createErrorEmbed("An error occurred while claiming your daily reward.")], flags: MessageFlags.Ephemeral });
} }
} }
}); });

View File

@@ -1,9 +1,10 @@
import { createCommand } from "@/lib/utils"; import { createCommand } from "@/lib/utils";
import { SlashCommandBuilder, EmbedBuilder } from "discord.js"; import { SlashCommandBuilder, EmbedBuilder, MessageFlags } from "discord.js";
import { economyService } from "@/modules/economy/economy.service"; import { economyService } from "@/modules/economy/economy.service";
import { userService } from "@/modules/user/user.service"; import { userService } from "@/modules/user/user.service";
import { config } from "@/lib/config"; import { config } from "@/lib/config";
import { createErrorEmbed, createWarningEmbed } from "@lib/embeds"; import { createErrorEmbed, createWarningEmbed } from "@/lib/embeds";
import { UserError } from "@/lib/errors";
export const pay = createCommand({ export const pay = createCommand({
data: new SlashCommandBuilder() data: new SlashCommandBuilder()
@@ -27,12 +28,12 @@ export const pay = createCommand({
const receiverId = targetUser.id; const receiverId = targetUser.id;
if (amount < config.economy.transfers.minAmount) { if (amount < config.economy.transfers.minAmount) {
await interaction.reply({ embeds: [createWarningEmbed(`Amount must be at least ${config.economy.transfers.minAmount}.`)], ephemeral: true }); await interaction.reply({ embeds: [createWarningEmbed(`Amount must be at least ${config.economy.transfers.minAmount}.`)], flags: MessageFlags.Ephemeral });
return; return;
} }
if (senderId === receiverId) { if (senderId === receiverId) {
await interaction.reply({ embeds: [createWarningEmbed("You cannot pay yourself.")], ephemeral: true }); await interaction.reply({ embeds: [createWarningEmbed("You cannot pay yourself.")], flags: MessageFlags.Ephemeral });
return; return;
} }
@@ -48,12 +49,12 @@ export const pay = createCommand({
await interaction.reply({ embeds: [embed] }); await interaction.reply({ embeds: [embed] });
} catch (error: any) { } catch (error: any) {
if (error.message.includes("Insufficient funds")) { if (error instanceof UserError) {
await interaction.reply({ embeds: [createWarningEmbed("Insufficient funds.")], ephemeral: true }); await interaction.reply({ embeds: [createWarningEmbed(error.message)], flags: MessageFlags.Ephemeral });
return; return;
} }
console.error(error); console.error(error);
await interaction.reply({ embeds: [createErrorEmbed("Transfer failed.")], ephemeral: true }); await interaction.reply({ embeds: [createErrorEmbed("Transfer failed due to an unexpected error.")], flags: MessageFlags.Ephemeral });
} }
} }
}); });

View File

@@ -1,5 +1,5 @@
import { createCommand } from "@/lib/utils"; import { createCommand } from "@/lib/utils";
import { SlashCommandBuilder, EmbedBuilder, ChannelType, ActionRowBuilder, ButtonBuilder, ButtonStyle, ThreadAutoArchiveDuration } from "discord.js"; import { SlashCommandBuilder, EmbedBuilder, ChannelType, ActionRowBuilder, ButtonBuilder, ButtonStyle, ThreadAutoArchiveDuration, MessageFlags } from "discord.js";
import { TradeService } from "@/modules/trade/trade.service"; import { TradeService } from "@/modules/trade/trade.service";
import { createErrorEmbed, createWarningEmbed } from "@lib/embeds"; import { createErrorEmbed, createWarningEmbed } from "@lib/embeds";
@@ -16,19 +16,19 @@ export const trade = createCommand({
const targetUser = interaction.options.getUser("user", true); const targetUser = interaction.options.getUser("user", true);
if (targetUser.id === interaction.user.id) { if (targetUser.id === interaction.user.id) {
await interaction.reply({ embeds: [createWarningEmbed("You cannot trade with yourself.")], ephemeral: true }); await interaction.reply({ embeds: [createWarningEmbed("You cannot trade with yourself.")], flags: MessageFlags.Ephemeral });
return; return;
} }
if (targetUser.bot) { if (targetUser.bot) {
await interaction.reply({ embeds: [createWarningEmbed("You cannot trade with bots.")], ephemeral: true }); await interaction.reply({ embeds: [createWarningEmbed("You cannot trade with bots.")], flags: MessageFlags.Ephemeral });
return; return;
} }
// Create Thread // Create Thread
const channel = interaction.channel; const channel = interaction.channel;
if (!channel || channel.type === ChannelType.DM) { if (!channel || channel.type === ChannelType.DM) {
await interaction.reply({ embeds: [createErrorEmbed("Cannot start trade in DMs.")], ephemeral: true }); await interaction.reply({ embeds: [createErrorEmbed("Cannot start trade in DMs.")], flags: MessageFlags.Ephemeral });
return; return;
} }
@@ -53,7 +53,7 @@ export const trade = createCommand({
} catch (err) { } catch (err) {
console.error("Failed to delete setup message", err); console.error("Failed to delete setup message", err);
} }
await interaction.followUp({ embeds: [createErrorEmbed("Failed to create trade thread. Check permissions.")], ephemeral: true }); await interaction.followUp({ embeds: [createErrorEmbed("Failed to create trade thread. Check permissions.")], flags: MessageFlags.Ephemeral });
return; return;
} }

View File

@@ -1,5 +1,5 @@
import { createCommand } from "@/lib/utils"; import { createCommand } from "@/lib/utils";
import { SlashCommandBuilder, EmbedBuilder } from "discord.js"; import { SlashCommandBuilder, EmbedBuilder, MessageFlags } from "discord.js";
import { questService } from "@/modules/quest/quest.service"; import { questService } from "@/modules/quest/quest.service";
import { createWarningEmbed } from "@lib/embeds"; import { createWarningEmbed } from "@lib/embeds";
@@ -8,7 +8,7 @@ export const quests = createCommand({
.setName("quests") .setName("quests")
.setDescription("View your active quests"), .setDescription("View your active quests"),
execute: async (interaction) => { execute: async (interaction) => {
await interaction.deferReply({ ephemeral: true }); await interaction.deferReply({ flags: MessageFlags.Ephemeral });
const userQuests = await questService.getUserQuests(interaction.user.id); const userQuests = await questService.getUserQuests(interaction.user.id);

View File

@@ -1,6 +1,6 @@
import { createCommand } from "@lib/utils"; import { createCommand } from "@lib/utils";
import { KyokoClient } from "@/lib/BotClient"; import { KyokoClient } from "@/lib/BotClient";
import { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits } from "discord.js"; import { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits, MessageFlags } from "discord.js";
import { createErrorEmbed } from "@lib/embeds"; import { createErrorEmbed } from "@lib/embeds";
export const reload = createCommand({ export const reload = createCommand({
@@ -9,7 +9,7 @@ export const reload = createCommand({
.setDescription("Reloads all commands") .setDescription("Reloads all commands")
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator), .setDefaultMemberPermissions(PermissionFlagsBits.Administrator),
execute: async (interaction) => { execute: async (interaction) => {
await interaction.deferReply({ ephemeral: true }); await interaction.deferReply({ flags: MessageFlags.Ephemeral });
try { try {
await KyokoClient.loadCommands(true); await KyokoClient.loadCommands(true);

View File

@@ -1,5 +1,5 @@
import { createCommand } from "@/lib/utils"; import { createCommand } from "@/lib/utils";
import { SlashCommandBuilder, PermissionFlagsBits, TextChannel, NewsChannel, VoiceChannel } from "discord.js"; import { SlashCommandBuilder, PermissionFlagsBits, TextChannel, NewsChannel, VoiceChannel, MessageFlags } from "discord.js";
import { createErrorEmbed } from "@/lib/embeds"; import { createErrorEmbed } from "@/lib/embeds";
export const webhook = createCommand({ export const webhook = createCommand({
@@ -13,7 +13,7 @@ export const webhook = createCommand({
.setRequired(true) .setRequired(true)
), ),
execute: async (interaction) => { execute: async (interaction) => {
await interaction.deferReply({ ephemeral: true }); await interaction.deferReply({ flags: MessageFlags.Ephemeral });
const payloadString = interaction.options.getString("payload", true); const payloadString = interaction.options.getString("payload", true);
let payload; let payload;

15
src/lib/db.ts Normal file
View File

@@ -0,0 +1,15 @@
import { DrizzleClient } from "./DrizzleClient";
import type { Transaction } from "./types";
export const withTransaction = async <T>(
callback: (tx: Transaction) => Promise<T>,
tx?: Transaction
): Promise<T> => {
if (tx) {
return await callback(tx);
} else {
return await DrizzleClient.transaction(async (newTx) => {
return await callback(newTx);
});
}
};

18
src/lib/errors.ts Normal file
View File

@@ -0,0 +1,18 @@
export class ApplicationError extends Error {
constructor(message: string) {
super(message);
this.name = this.constructor.name;
}
}
export class UserError extends ApplicationError {
constructor(message: string) {
super(message);
}
}
export class SystemError extends ApplicationError {
constructor(message: string) {
super(message);
}
}

View File

@@ -11,3 +11,8 @@ export interface Event<K extends keyof ClientEvents> {
once?: boolean; once?: boolean;
execute: (...args: ClientEvents[K]) => Promise<void> | void; execute: (...args: ClientEvents[K]) => Promise<void> | void;
} }
import { DrizzleClient } from "./DrizzleClient";
export type DbClient = typeof DrizzleClient;
export type Transaction = Parameters<Parameters<DbClient['transaction']>[0]>[0];

View File

@@ -1,30 +1,32 @@
import { users, transactions, userTimers } from "@/db/schema"; import { users, transactions, userTimers } from "@/db/schema";
import { eq, sql, and } from "drizzle-orm"; import { eq, sql, and } from "drizzle-orm";
import { DrizzleClient } from "@/lib/DrizzleClient";
import { config } from "@/lib/config"; import { config } from "@/lib/config";
import { withTransaction } from "@/lib/db";
import type { Transaction } from "@/lib/types";
import { UserError } from "@/lib/errors";
export const economyService = { export const economyService = {
transfer: async (fromUserId: string, toUserId: string, amount: bigint, tx?: any) => { transfer: async (fromUserId: string, toUserId: string, amount: bigint, tx?: Transaction) => {
if (amount <= 0n) { if (amount <= 0n) {
throw new Error("Amount must be positive"); throw new UserError("Amount must be positive");
} }
if (fromUserId === toUserId) { if (fromUserId === toUserId) {
throw new Error("Cannot transfer to self"); throw new UserError("Cannot transfer to self");
} }
const execute = async (txFn: any) => { return await withTransaction(async (txFn) => {
// Check sender balance // Check sender balance
const sender = await txFn.query.users.findFirst({ const sender = await txFn.query.users.findFirst({
where: eq(users.id, BigInt(fromUserId)), where: eq(users.id, BigInt(fromUserId)),
}); });
if (!sender) { if (!sender) {
throw new Error("Sender not found"); throw new UserError("Sender not found");
} }
if ((sender.balance ?? 0n) < amount) { if ((sender.balance ?? 0n) < amount) {
throw new Error("Insufficient funds"); throw new UserError("Insufficient funds");
} }
// Deduct from sender // Deduct from sender
@@ -59,19 +61,11 @@ export const economyService = {
}); });
return { success: true, amount }; return { success: true, amount };
}; }, tx);
if (tx) {
return await execute(tx);
} else {
return await DrizzleClient.transaction(async (t) => {
return await execute(t);
});
}
}, },
claimDaily: async (userId: string, tx?: any) => { claimDaily: async (userId: string, tx?: Transaction) => {
const execute = async (txFn: any) => { return await withTransaction(async (txFn) => {
const now = new Date(); const now = new Date();
const startOfDay = new Date(now); const startOfDay = new Date(now);
startOfDay.setHours(0, 0, 0, 0); startOfDay.setHours(0, 0, 0, 0);
@@ -86,7 +80,7 @@ export const economyService = {
}); });
if (cooldown && cooldown.expiresAt > now) { if (cooldown && cooldown.expiresAt > now) {
throw new Error(`Daily already claimed. Ready at ${cooldown.expiresAt}`); throw new UserError(`Daily already claimed. Ready at ${cooldown.expiresAt}`);
} }
// Get user for streak logic // Get user for streak logic
@@ -95,7 +89,7 @@ export const economyService = {
}); });
if (!user) { if (!user) {
throw new Error("User not found"); throw new Error("User not found"); // This might be system error because user should exist if authenticated, but keeping simple for now
} }
let streak = (user.dailyStreak || 0) + 1; let streak = (user.dailyStreak || 0) + 1;
@@ -145,26 +139,18 @@ export const economyService = {
}); });
return { claimed: true, amount: totalReward, streak, nextReadyAt }; return { claimed: true, amount: totalReward, streak, nextReadyAt };
}; }, tx);
if (tx) {
return await execute(tx);
} else {
return await DrizzleClient.transaction(async (t) => {
return await execute(t);
});
}
}, },
modifyUserBalance: async (id: string, amount: bigint, type: string, description: string, relatedUserId?: string | null, tx?: any) => { modifyUserBalance: async (id: string, amount: bigint, type: string, description: string, relatedUserId?: string | null, tx?: Transaction) => {
const execute = async (txFn: any) => { return await withTransaction(async (txFn) => {
if (amount < 0n) { if (amount < 0n) {
// Check sufficient funds if removing // Check sufficient funds if removing
const user = await txFn.query.users.findFirst({ const user = await txFn.query.users.findFirst({
where: eq(users.id, BigInt(id)) where: eq(users.id, BigInt(id))
}); });
if (!user || (user.balance ?? 0n) < -amount) { if (!user || (user.balance ?? 0n) < -amount) {
throw new Error("Insufficient funds"); throw new UserError("Insufficient funds");
} }
} }
@@ -184,14 +170,6 @@ export const economyService = {
}); });
return user; return user;
}; }, tx);
if (tx) {
return await execute(tx);
} else {
return await DrizzleClient.transaction(async (t) => {
return await execute(t);
});
}
}, },
}; };

View File

@@ -1,13 +1,14 @@
import { inventory, items, users } from "@/db/schema"; import { inventory, items, users } from "@/db/schema";
import { eq, and, sql, count } from "drizzle-orm"; import { eq, and, sql, count } from "drizzle-orm";
import { DrizzleClient } from "@/lib/DrizzleClient"; import { DrizzleClient } from "@/lib/DrizzleClient";
import { economyService } from "@/modules/economy/economy.service"; import { economyService } from "@/modules/economy/economy.service";
import { config } from "@/lib/config"; import { config } from "@/lib/config";
import { withTransaction } from "@/lib/db";
import type { Transaction } from "@/lib/types";
export const inventoryService = { export const inventoryService = {
addItem: async (userId: string, itemId: number, quantity: bigint = 1n, tx?: any) => { addItem: async (userId: string, itemId: number, quantity: bigint = 1n, tx?: Transaction) => {
const execute = async (txFn: any) => { return await withTransaction(async (txFn) => {
// Check if item exists in inventory // Check if item exists in inventory
const existing = await txFn.query.inventory.findFirst({ const existing = await txFn.query.inventory.findFirst({
where: and( where: and(
@@ -39,7 +40,7 @@ export const inventoryService = {
.from(inventory) .from(inventory)
.where(eq(inventory.userId, BigInt(userId))); .where(eq(inventory.userId, BigInt(userId)));
if (inventoryCount.count >= config.inventory.maxSlots) { if (inventoryCount && inventoryCount.count >= config.inventory.maxSlots) {
throw new Error(`Inventory full (Max ${config.inventory.maxSlots} slots)`); throw new Error(`Inventory full (Max ${config.inventory.maxSlots} slots)`);
} }
@@ -56,12 +57,11 @@ export const inventoryService = {
.returning(); .returning();
return entry; return entry;
} }
}; }, tx);
return tx ? await execute(tx) : await DrizzleClient.transaction(execute);
}, },
removeItem: async (userId: string, itemId: number, quantity: bigint = 1n, tx?: any) => { removeItem: async (userId: string, itemId: number, quantity: bigint = 1n, tx?: Transaction) => {
const execute = async (txFn: any) => { return await withTransaction(async (txFn) => {
const existing = await txFn.query.inventory.findFirst({ const existing = await txFn.query.inventory.findFirst({
where: and( where: and(
eq(inventory.userId, BigInt(userId)), eq(inventory.userId, BigInt(userId)),
@@ -93,8 +93,7 @@ export const inventoryService = {
.returning(); .returning();
return entry; return entry;
} }
}; }, tx);
return tx ? await execute(tx) : await DrizzleClient.transaction(execute);
}, },
getInventory: async (userId: string) => { getInventory: async (userId: string) => {
@@ -106,8 +105,8 @@ export const inventoryService = {
}); });
}, },
buyItem: async (userId: string, itemId: number, quantity: bigint = 1n, tx?: any) => { buyItem: async (userId: string, itemId: number, quantity: bigint = 1n, tx?: Transaction) => {
const execute = async (txFn: any) => { return await withTransaction(async (txFn) => {
const item = await txFn.query.items.findFirst({ const item = await txFn.query.items.findFirst({
where: eq(items.id, itemId), where: eq(items.id, itemId),
}); });
@@ -123,9 +122,7 @@ export const inventoryService = {
await inventoryService.addItem(userId, itemId, quantity, txFn); await inventoryService.addItem(userId, itemId, quantity, txFn);
return { success: true, item, totalPrice }; return { success: true, item, totalPrice };
}; }, tx);
return tx ? await execute(tx) : await DrizzleClient.transaction(execute);
}, },
getItem: async (itemId: number) => { getItem: async (itemId: number) => {

View File

@@ -1,7 +1,8 @@
import { users, userTimers } from "@/db/schema"; import { users, userTimers } from "@/db/schema";
import { eq, sql, and } from "drizzle-orm"; import { eq, sql, and } from "drizzle-orm";
import { DrizzleClient } from "@/lib/DrizzleClient"; import { withTransaction } from "@/lib/db";
import { config } from "@/lib/config"; import { config } from "@/lib/config";
import type { Transaction } from "@/lib/types";
export const levelingService = { export const levelingService = {
// Calculate XP required for a specific level // Calculate XP required for a specific level
@@ -10,8 +11,8 @@ export const levelingService = {
}, },
// Pure XP addition - No cooldown checks // Pure XP addition - No cooldown checks
addXp: async (id: string, amount: bigint, tx?: any) => { addXp: async (id: string, amount: bigint, tx?: Transaction) => {
const execute = async (txFn: any) => { return await withTransaction(async (txFn) => {
// Get current state // Get current state
const user = await txFn.query.users.findFirst({ const user = await txFn.query.users.findFirst({
where: eq(users.id, BigInt(id)), where: eq(users.id, BigInt(id)),
@@ -43,20 +44,12 @@ export const levelingService = {
.returning(); .returning();
return { user: updatedUser, levelUp, currentLevel }; return { user: updatedUser, levelUp, currentLevel };
}; }, tx);
if (tx) {
return await execute(tx);
} else {
return await DrizzleClient.transaction(async (t) => {
return await execute(t);
})
}
}, },
// Handle chat XP with cooldowns // Handle chat XP with cooldowns
processChatXp: async (id: string, tx?: any) => { processChatXp: async (id: string, tx?: Transaction) => {
const execute = async (txFn: any) => { return await withTransaction(async (txFn) => {
// check if an xp cooldown is in place // check if an xp cooldown is in place
const cooldown = await txFn.query.userTimers.findFirst({ const cooldown = await txFn.query.userTimers.findFirst({
where: and( where: and(
@@ -93,14 +86,6 @@ export const levelingService = {
}); });
return { awarded: true, amount, ...result }; return { awarded: true, amount, ...result };
}; }, tx);
if (tx) {
return await execute(tx);
} else {
return await DrizzleClient.transaction(async (t) => {
return await execute(t);
})
}
} }
}; };

View File

@@ -4,10 +4,12 @@ import { eq, and, sql } from "drizzle-orm";
import { DrizzleClient } from "@/lib/DrizzleClient"; import { DrizzleClient } from "@/lib/DrizzleClient";
import { economyService } from "@/modules/economy/economy.service"; import { economyService } from "@/modules/economy/economy.service";
import { levelingService } from "@/modules/leveling/leveling.service"; import { levelingService } from "@/modules/leveling/leveling.service";
import { withTransaction } from "@/lib/db";
import type { Transaction } from "@/lib/types";
export const questService = { export const questService = {
assignQuest: async (userId: string, questId: number, tx?: any) => { assignQuest: async (userId: string, questId: number, tx?: Transaction) => {
const execute = async (txFn: any) => { return await withTransaction(async (txFn) => {
return await txFn.insert(userQuests) return await txFn.insert(userQuests)
.values({ .values({
userId: BigInt(userId), userId: BigInt(userId),
@@ -16,12 +18,11 @@ export const questService = {
}) })
.onConflictDoNothing() // Ignore if already assigned .onConflictDoNothing() // Ignore if already assigned
.returning(); .returning();
}; }, tx);
return tx ? await execute(tx) : await DrizzleClient.transaction(execute);
}, },
updateProgress: async (userId: string, questId: number, progress: number, tx?: any) => { updateProgress: async (userId: string, questId: number, progress: number, tx?: Transaction) => {
const execute = async (txFn: any) => { return await withTransaction(async (txFn) => {
return await txFn.update(userQuests) return await txFn.update(userQuests)
.set({ progress: progress }) .set({ progress: progress })
.where(and( .where(and(
@@ -29,12 +30,11 @@ export const questService = {
eq(userQuests.questId, questId) eq(userQuests.questId, questId)
)) ))
.returning(); .returning();
}; }, tx);
return tx ? await execute(tx) : await DrizzleClient.transaction(execute);
}, },
completeQuest: async (userId: string, questId: number, tx?: any) => { completeQuest: async (userId: string, questId: number, tx?: Transaction) => {
const execute = async (txFn: any) => { return await withTransaction(async (txFn) => {
const userQuest = await txFn.query.userQuests.findFirst({ const userQuest = await txFn.query.userQuests.findFirst({
where: and( where: and(
eq(userQuests.userId, BigInt(userId)), eq(userQuests.userId, BigInt(userId)),
@@ -73,9 +73,7 @@ export const questService = {
} }
return { success: true, rewards: results }; return { success: true, rewards: results };
}; }, tx);
return tx ? await execute(tx) : await DrizzleClient.transaction(execute);
}, },
getUserQuests: async (userId: string) => { getUserQuests: async (userId: string) => {

View File

@@ -3,6 +3,7 @@ import { DrizzleClient } from "@/lib/DrizzleClient";
import { economyService } from "@/modules/economy/economy.service"; import { economyService } from "@/modules/economy/economy.service";
import { inventoryService } from "@/modules/inventory/inventory.service"; import { inventoryService } from "@/modules/inventory/inventory.service";
import { itemTransactions } from "@/db/schema"; import { itemTransactions } from "@/db/schema";
import type { Transaction } from "@/lib/types";
export class TradeService { export class TradeService {
private static sessions = new Map<string, TradeSession>(); private static sessions = new Map<string, TradeSession>();
@@ -136,7 +137,7 @@ export class TradeService {
this.endSession(threadId); this.endSession(threadId);
} }
private static async processTransfer(tx: any, from: TradeParticipant, to: TradeParticipant, threadId: string) { private static async processTransfer(tx: Transaction, from: TradeParticipant, to: TradeParticipant, threadId: string) {
// 1. Money // 1. Money
if (from.offer.money > 0n) { if (from.offer.money > 0n) {
await economyService.modifyUserBalance( await economyService.modifyUserBalance(