From b4e68a46bfb74478d68a91c4007961597f3a91ce Mon Sep 17 00:00:00 2001 From: Merul Dhiman Date: Mon, 15 Dec 2025 20:08:05 +0530 Subject: [PATCH 01/10] feat: add burn button and auto reload --- .../currency/add-currency-account-modal.tsx | 8 +++++- .../client/src/pages/currency-detail.tsx | 28 +++++++++++++------ 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/platforms/eCurrency/client/src/components/currency/add-currency-account-modal.tsx b/platforms/eCurrency/client/src/components/currency/add-currency-account-modal.tsx index 034a152e..609d0a3a 100644 --- a/platforms/eCurrency/client/src/components/currency/add-currency-account-modal.tsx +++ b/platforms/eCurrency/client/src/components/currency/add-currency-account-modal.tsx @@ -72,7 +72,13 @@ export default function AddCurrencyAccountModal({ open, onOpenChange, accountCon return response.data; }, onSuccess: () => { - queryClient.invalidateQueries({ queryKey: balancesQueryKey }); + // Refresh balances and history for the active context right after adding + queryClient.invalidateQueries({ + predicate: (q) => { + const key = q.queryKey?.[0]; + return key === "balances" || key === "history" || key === "accountDetails"; + }, + }); setCurrencyId(""); setSearchQuery(""); setIsOpen(false); diff --git a/platforms/eCurrency/client/src/pages/currency-detail.tsx b/platforms/eCurrency/client/src/pages/currency-detail.tsx index b82819f7..f1fa35bf 100644 --- a/platforms/eCurrency/client/src/pages/currency-detail.tsx +++ b/platforms/eCurrency/client/src/pages/currency-detail.tsx @@ -6,7 +6,7 @@ import TransferModal from "../components/currency/transfer-modal"; import TransactionDetailModal from "../components/currency/transaction-detail-modal"; import MintCurrencyModal from "../components/currency/mint-currency-modal"; import UserMenuDropdown from "../components/user-menu-dropdown"; -import { Send, Wallet, Sparkles, ChevronLeft } from "lucide-react"; +import { Send, Wallet, Sparkles, ChevronLeft, Flame } from "lucide-react"; import { useState, useEffect } from "react"; import { formatEName } from "../lib/utils"; import TransactionCard from "../components/currency/transaction-card"; @@ -375,13 +375,25 @@ export default function CurrencyDetail() {

Transactions

{isAdminOfCurrency && accountContext?.type === "group" && accountContext.id === currency.groupId && ( - + <> + + + )}
+ + {/* Hashes (conditional) */} + {(transaction.hash || transaction.prevHash) && ( +
+ {transaction.hash && ( +
+
+

Transaction Hash

+ +
+

{transaction.hash}

+
+ )} + {transaction.prevHash && ( +
+
+

Previous Hash

+ +
+

{transaction.prevHash}

+
+ )} +
+ )} ) : (
From c4de3733ff17dcbe7f5596c6c84ea3cbe64ca9b8 Mon Sep 17 00:00:00 2001 From: Merul Dhiman Date: Mon, 15 Dec 2025 21:17:31 +0530 Subject: [PATCH 03/10] feat: make burn work properly --- .../src/controllers/LedgerController.ts | 58 +++++++++ platforms/eCurrency-api/src/index.ts | 1 + .../src/services/LedgerService.ts | 48 ++++++++ .../client/src/pages/currency-detail.tsx | 113 ++++++++++++++++-- 4 files changed, 213 insertions(+), 7 deletions(-) diff --git a/platforms/eCurrency-api/src/controllers/LedgerController.ts b/platforms/eCurrency-api/src/controllers/LedgerController.ts index 0383351f..84bb909a 100644 --- a/platforms/eCurrency-api/src/controllers/LedgerController.ts +++ b/platforms/eCurrency-api/src/controllers/LedgerController.ts @@ -2,14 +2,17 @@ import { Request, Response } from "express"; import { LedgerService } from "../services/LedgerService"; import { AccountType } from "../database/entities/Ledger"; import { GroupService } from "../services/GroupService"; +import { CurrencyService } from "../services/CurrencyService"; export class LedgerController { private ledgerService: LedgerService; private groupService: GroupService; + private currencyService: CurrencyService; constructor() { this.ledgerService = new LedgerService(); this.groupService = new GroupService(); + this.currencyService = new CurrencyService(); } getBalance = async (req: Request, res: Response) => { @@ -293,6 +296,61 @@ export class LedgerController { } }; + burn = async (req: Request, res: Response) => { + try { + if (!req.user) { + return res.status(401).json({ error: "Authentication required" }); + } + + const { currencyId, amount, description } = req.body; + + if (!currencyId || amount === undefined || amount === null) { + return res.status(400).json({ error: "currencyId and amount are required" }); + } + + const parsedAmount = Number(amount); + if (Number.isNaN(parsedAmount) || parsedAmount <= 0) { + return res.status(400).json({ error: "Amount must be a positive number" }); + } + + const currency = await this.currencyService.getCurrencyById(currencyId); + if (!currency) { + return res.status(404).json({ error: "Currency not found" }); + } + + // Only group admins of the currency's group can burn (treasury) + const isAdmin = await this.groupService.isGroupAdmin(currency.groupId, req.user.id); + if (!isAdmin) { + return res.status(403).json({ error: "Only group admins can burn from treasury" }); + } + + const burnEntry = await this.ledgerService.burn( + currencyId, + currency.groupId, + parsedAmount, + description + ); + + res.status(200).json({ + message: "Burn successful", + transaction: { + id: burnEntry.id, + amount: burnEntry.amount, + balance: burnEntry.balance, + createdAt: burnEntry.createdAt, + hash: burnEntry.hash, + prevHash: burnEntry.prevHash, + }, + }); + } catch (error: any) { + console.error("Error burning currency:", error); + if (error.message.includes("Insufficient balance") || error.message.includes("not found")) { + return res.status(400).json({ error: error.message }); + } + res.status(500).json({ error: "Internal server error" }); + } + }; + initializeAccount = async (req: Request, res: Response) => { try { if (!req.user) { diff --git a/platforms/eCurrency-api/src/index.ts b/platforms/eCurrency-api/src/index.ts index 5a052bdc..4a483f5c 100644 --- a/platforms/eCurrency-api/src/index.ts +++ b/platforms/eCurrency-api/src/index.ts @@ -118,6 +118,7 @@ app.patch("/api/currencies/:id/max-negative", authGuard, currencyController.upda app.get("/api/ledger/balance", authGuard, ledgerController.getBalance); app.get("/api/ledger/balance/:currencyId", authGuard, ledgerController.getBalanceByCurrencyId); app.post("/api/ledger/transfer", authGuard, ledgerController.transfer); +app.post("/api/ledger/burn", authGuard, ledgerController.burn); app.get("/api/ledger/history", authGuard, ledgerController.getHistory); app.get("/api/ledger/history/:currencyId", authGuard, ledgerController.getHistory); app.get("/api/ledger/transaction/:id", authGuard, ledgerController.getTransactionById); diff --git a/platforms/eCurrency-api/src/services/LedgerService.ts b/platforms/eCurrency-api/src/services/LedgerService.ts index 154c2d95..1a9fe838 100644 --- a/platforms/eCurrency-api/src/services/LedgerService.ts +++ b/platforms/eCurrency-api/src/services/LedgerService.ts @@ -197,6 +197,54 @@ export class LedgerService { return { debit, credit }; } + async burn( + currencyId: string, + groupId: string, + amount: number, + description?: string + ): Promise { + const currency = await this.currencyRepository.findOne({ where: { id: currencyId } }); + if (!currency) { + throw new Error("Currency not found"); + } + + // Ensure treasury account exists + await this.initializeAccount(currencyId, groupId, AccountType.GROUP); + + const currentBalance = await this.getAccountBalance(currencyId, groupId, AccountType.GROUP); + + // Enforce bounds + if (!currency.allowNegative && currentBalance < amount) { + throw new Error("Insufficient balance. Negative balances are not allowed."); + } + + if (currency.allowNegative && currency.maxNegativeBalance !== null && currency.maxNegativeBalance !== undefined) { + const newBalance = currentBalance - amount; + if (newBalance < Number(currency.maxNegativeBalance)) { + throw new Error(`Insufficient balance. This currency allows negative balances down to ${currency.maxNegativeBalance}.`); + } + } + + const burnDescription = description || `Burned ${amount} ${currency.name}`; + + // Single debit entry against treasury + const debit = await this.addLedgerEntry( + currencyId, + groupId, + AccountType.GROUP, + amount, + LedgerType.DEBIT, + burnDescription, + groupId, + AccountType.GROUP, + null, + null, + currentBalance + ); + + return debit; + } + async getTransactionHistory( currencyId?: string, accountId?: string, diff --git a/platforms/eCurrency/client/src/pages/currency-detail.tsx b/platforms/eCurrency/client/src/pages/currency-detail.tsx index f1fa35bf..9db0b332 100644 --- a/platforms/eCurrency/client/src/pages/currency-detail.tsx +++ b/platforms/eCurrency/client/src/pages/currency-detail.tsx @@ -18,6 +18,11 @@ export default function CurrencyDetail() { const queryClient = useQueryClient(); const [transferOpen, setTransferOpen] = useState(false); const [mintOpen, setMintOpen] = useState(false); + const [burnOpen, setBurnOpen] = useState(false); + const [burnAmount, setBurnAmount] = useState(""); + const [burnReason, setBurnReason] = useState(""); + const [burnError, setBurnError] = useState(null); + const [burnSaving, setBurnSaving] = useState(false); const [selectedTransactionId, setSelectedTransactionId] = useState(null); const [transactionOffset, setTransactionOffset] = useState(0); const [allTransactions, setAllTransactions] = useState([]); @@ -234,6 +239,42 @@ export default function CurrencyDetail() { return
Currency not found
; } + const handleBurn = async () => { + if (!currencyId) return; + setBurnError(null); + setBurnSaving(true); + try { + const amountNum = parseFloat(burnAmount); + if (Number.isNaN(amountNum) || amountNum <= 0) { + setBurnError("Enter a valid amount greater than zero."); + setBurnSaving(false); + return; + } + + await apiClient.post("/api/ledger/burn", { + currencyId, + amount: amountNum, + description: burnReason || undefined, + }); + + await Promise.all([ + queryClient.invalidateQueries({ queryKey: ["balance", currencyId, accountContext] }), + queryClient.invalidateQueries({ queryKey: ["balances", accountContext] }), + queryClient.invalidateQueries({ queryKey: ["history", currencyId, accountContext, transactionOffset] }), + queryClient.invalidateQueries({ queryKey: ["totalSupply", currencyId] }), + ]); + + setBurnAmount(""); + setBurnReason(""); + setBurnOpen(false); + } catch (error: any) { + const message = error?.response?.data?.error || error?.message || "Burn failed"; + setBurnError(message); + } finally { + setBurnSaving(false); + } + }; + return (
{/* Header - Same as dashboard */} @@ -375,7 +416,7 @@ export default function CurrencyDetail() {

Transactions

{isAdminOfCurrency && accountContext?.type === "group" && accountContext.id === currency.groupId && ( - <> + <> - + )} +
+
+
+ + setBurnAmount(e.target.value)} + className="w-full px-3 py-2 border rounded-md" + placeholder="0.00" + /> +
+
+ + setBurnReason(e.target.value)} + className="w-full px-3 py-2 border rounded-md" + placeholder="e.g., supply reduction" + /> +
+ {burnError && ( +
+ {burnError} +
+ )} +
+ + +
+
+
+
+ )} ); } From 2899c82a62b7d495b93042f0cfb2346a706afe4c Mon Sep 17 00:00:00 2001 From: Merul Dhiman Date: Mon, 15 Dec 2025 21:21:04 +0530 Subject: [PATCH 04/10] fix: build --- platforms/eCurrency-api/src/services/LedgerService.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/platforms/eCurrency-api/src/services/LedgerService.ts b/platforms/eCurrency-api/src/services/LedgerService.ts index 1a9fe838..c6cd693c 100644 --- a/platforms/eCurrency-api/src/services/LedgerService.ts +++ b/platforms/eCurrency-api/src/services/LedgerService.ts @@ -237,8 +237,8 @@ export class LedgerService { burnDescription, groupId, AccountType.GROUP, - null, - null, + undefined, + undefined, currentBalance ); From 61938d19413f67b65c15238ed135404391d93350 Mon Sep 17 00:00:00 2001 From: Merul Dhiman Date: Mon, 15 Dec 2025 21:22:50 +0530 Subject: [PATCH 05/10] chore: migration --- .../migrations/1765809758435-migration.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 platforms/eCurrency-api/src/database/migrations/1765809758435-migration.ts diff --git a/platforms/eCurrency-api/src/database/migrations/1765809758435-migration.ts b/platforms/eCurrency-api/src/database/migrations/1765809758435-migration.ts new file mode 100644 index 00000000..8260d249 --- /dev/null +++ b/platforms/eCurrency-api/src/database/migrations/1765809758435-migration.ts @@ -0,0 +1,16 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class Migration1765809758435 implements MigrationInterface { + name = 'Migration1765809758435' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "ledger" ADD "hash" text`); + await queryRunner.query(`ALTER TABLE "ledger" ADD "prevHash" text`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "ledger" DROP COLUMN "prevHash"`); + await queryRunner.query(`ALTER TABLE "ledger" DROP COLUMN "hash"`); + } + +} From 764c57049c94fee9c6aa2b895989341615d06b22 Mon Sep 17 00:00:00 2001 From: Merul Dhiman Date: Mon, 15 Dec 2025 21:29:36 +0530 Subject: [PATCH 06/10] chore: fix icon --- platforms/eCurrency/client/src/pages/currency-detail.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platforms/eCurrency/client/src/pages/currency-detail.tsx b/platforms/eCurrency/client/src/pages/currency-detail.tsx index 9db0b332..d92d525b 100644 --- a/platforms/eCurrency/client/src/pages/currency-detail.tsx +++ b/platforms/eCurrency/client/src/pages/currency-detail.tsx @@ -6,7 +6,7 @@ import TransferModal from "../components/currency/transfer-modal"; import TransactionDetailModal from "../components/currency/transaction-detail-modal"; import MintCurrencyModal from "../components/currency/mint-currency-modal"; import UserMenuDropdown from "../components/user-menu-dropdown"; -import { Send, Wallet, Sparkles, ChevronLeft, Flame } from "lucide-react"; +import { Send, Wallet, Sparkles, ChevronLeft, Flame, X } from "lucide-react"; import { useState, useEffect } from "react"; import { formatEName } from "../lib/utils"; import TransactionCard from "../components/currency/transaction-card"; From 86a1aa30fbff102447e4e6e68ac399a16d30a040 Mon Sep 17 00:00:00 2001 From: Merul Dhiman Date: Mon, 15 Dec 2025 21:35:42 +0530 Subject: [PATCH 07/10] feat: better TX ui --- .../components/currency/transaction-card.tsx | 23 ++++++++++++------- .../client/src/pages/currency-detail.tsx | 2 +- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/platforms/eCurrency/client/src/components/currency/transaction-card.tsx b/platforms/eCurrency/client/src/components/currency/transaction-card.tsx index a27ba343..175bccc1 100644 --- a/platforms/eCurrency/client/src/components/currency/transaction-card.tsx +++ b/platforms/eCurrency/client/src/components/currency/transaction-card.tsx @@ -1,4 +1,4 @@ -import { ArrowLeft, ArrowRight, Sparkles } from "lucide-react"; +import { ArrowLeft, ArrowRight, Sparkles, Flame } from "lucide-react"; import { formatEName } from "../../lib/utils"; interface TransactionCardProps { @@ -9,10 +9,15 @@ interface TransactionCardProps { export default function TransactionCard({ transaction, currencyName, onClick }: TransactionCardProps) { // For credits: show "Received from X", for debits: show "Sent to X" - // For minted: show "Minted" + // For minted: show "Minted", for burned: show "Burned" let mainText = ""; - if (transaction.description?.includes("Minted")) { + const isMinted = transaction.description?.toLowerCase().includes("minted"); + const isBurned = transaction.description?.toLowerCase().includes("burned") || transaction.description?.toLowerCase().includes("burn"); + + if (isMinted) { mainText = "Minted"; + } else if (isBurned) { + mainText = "Burned"; } else if (transaction.type === "credit") { const senderName = transaction.sender?.name || formatEName(transaction.sender?.ename) || "Unknown"; mainText = `Received from ${senderName}`; @@ -21,8 +26,6 @@ export default function TransactionCard({ transaction, currencyName, onClick }: mainText = `Sent to ${receiverName}`; } - const isMinted = transaction.description?.includes("Minted"); - return (
{isMinted ? ( + ) : isBurned ? ( + ) : transaction.type === "credit" ? ( ) : ( diff --git a/platforms/eCurrency/client/src/pages/currency-detail.tsx b/platforms/eCurrency/client/src/pages/currency-detail.tsx index d92d525b..4e842a01 100644 --- a/platforms/eCurrency/client/src/pages/currency-detail.tsx +++ b/platforms/eCurrency/client/src/pages/currency-detail.tsx @@ -511,7 +511,7 @@ export default function CurrencyDetail() { {burnOpen && (
setBurnOpen(false)}>
e.stopPropagation()} >
From 57c27635b6c375ecf123af149e08dc7d68f74b58 Mon Sep 17 00:00:00 2001 From: Merul Dhiman Date: Mon, 15 Dec 2025 21:36:36 +0530 Subject: [PATCH 08/10] chore: switch account UX --- .../eCurrency/client/src/pages/currency-detail.tsx | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/platforms/eCurrency/client/src/pages/currency-detail.tsx b/platforms/eCurrency/client/src/pages/currency-detail.tsx index 4e842a01..d6fad198 100644 --- a/platforms/eCurrency/client/src/pages/currency-detail.tsx +++ b/platforms/eCurrency/client/src/pages/currency-detail.tsx @@ -140,12 +140,23 @@ export default function CurrencyDetail() { const handleAccountContextChange = (context: { type: "user" | "group"; id: string } | null) => { // If null is passed, default to user account const finalContext = context || (user ? { type: "user" as const, id: user.id } : null); + + // Check if context actually changed + const contextChanged = !accountContext || + accountContext.type !== finalContext?.type || + accountContext.id !== finalContext?.id; + setAccountContext(finalContext); if (finalContext) { localStorage.setItem("ecurrency_account_context", JSON.stringify(finalContext)); } else { localStorage.removeItem("ecurrency_account_context"); } + + // Navigate to dashboard when context changes + if (contextChanged) { + setLocation("/"); + } }; const { data: totalSupplyData, isLoading: totalSupplyLoading } = useQuery({ From fce16b6b945bf1f4f05a87743d22f79c306c73b7 Mon Sep 17 00:00:00 2001 From: Merul Dhiman Date: Mon, 15 Dec 2025 21:39:23 +0530 Subject: [PATCH 09/10] feat: better table ux --- .../components/currency/transaction-card.tsx | 17 +++++++++++++++-- .../client/src/pages/currency-detail.tsx | 2 +- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/platforms/eCurrency/client/src/components/currency/transaction-card.tsx b/platforms/eCurrency/client/src/components/currency/transaction-card.tsx index 175bccc1..4fe11af2 100644 --- a/platforms/eCurrency/client/src/components/currency/transaction-card.tsx +++ b/platforms/eCurrency/client/src/components/currency/transaction-card.tsx @@ -11,8 +11,21 @@ export default function TransactionCard({ transaction, currencyName, onClick }: // For credits: show "Received from X", for debits: show "Sent to X" // For minted: show "Minted", for burned: show "Burned" let mainText = ""; - const isMinted = transaction.description?.toLowerCase().includes("minted"); - const isBurned = transaction.description?.toLowerCase().includes("burned") || transaction.description?.toLowerCase().includes("burn"); + const desc = transaction.description?.toLowerCase() || ""; + // Check description for mint/burn keywords + const hasMintInDesc = desc.includes("minted") || desc.includes("mint"); + const hasBurnInDesc = desc.includes("burned") || desc.includes("burn"); + // Fallback: credit with no sender and group account type is likely a mint + const isLikelyMint = transaction.type === "credit" && + (!transaction.sender || (!transaction.sender.name && !transaction.sender.ename)) && + transaction.accountType === "group"; + // Debit with no receiver and group account type is likely a burn + const isLikelyBurn = transaction.type === "debit" && + (!transaction.receiver || (!transaction.receiver.name && !transaction.receiver.ename)) && + transaction.accountType === "group"; + + const isMinted = hasMintInDesc || isLikelyMint; + const isBurned = hasBurnInDesc || isLikelyBurn; if (isMinted) { mainText = "Minted"; diff --git a/platforms/eCurrency/client/src/pages/currency-detail.tsx b/platforms/eCurrency/client/src/pages/currency-detail.tsx index d6fad198..3b63fe78 100644 --- a/platforms/eCurrency/client/src/pages/currency-detail.tsx +++ b/platforms/eCurrency/client/src/pages/currency-detail.tsx @@ -570,7 +570,7 @@ export default function CurrencyDetail() { From 7cdf49d32f98e29588423265e354c9f34fd4c98a Mon Sep 17 00:00:00 2001 From: Merul Dhiman Date: Mon, 15 Dec 2025 21:41:24 +0530 Subject: [PATCH 10/10] feat: account creation better display --- .../components/currency/transaction-card.tsx | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/platforms/eCurrency/client/src/components/currency/transaction-card.tsx b/platforms/eCurrency/client/src/components/currency/transaction-card.tsx index 4fe11af2..465d9247 100644 --- a/platforms/eCurrency/client/src/components/currency/transaction-card.tsx +++ b/platforms/eCurrency/client/src/components/currency/transaction-card.tsx @@ -10,15 +10,28 @@ interface TransactionCardProps { export default function TransactionCard({ transaction, currencyName, onClick }: TransactionCardProps) { // For credits: show "Received from X", for debits: show "Sent to X" // For minted: show "Minted", for burned: show "Burned" + // For account initialization: show "Account Created" let mainText = ""; const desc = transaction.description?.toLowerCase() || ""; + const balance = Number(transaction.balance) || 0; + const amount = Math.abs(Number(transaction.amount)) || 0; + // Check description for mint/burn keywords const hasMintInDesc = desc.includes("minted") || desc.includes("mint"); const hasBurnInDesc = desc.includes("burned") || desc.includes("burn"); - // Fallback: credit with no sender and group account type is likely a mint + const hasInitInDesc = desc.includes("initialized") || desc.includes("account created"); + + // Account initialization: balance is 0, amount is 0 (or very close), no sender + const isAccountInit = balance === 0 && amount < 0.01 && + (!transaction.sender || (!transaction.sender.name && !transaction.sender.ename)) && + (hasInitInDesc || transaction.description === "Account initialized"); + + // Fallback: credit with no sender and group account type is likely a mint (but not if balance is 0) const isLikelyMint = transaction.type === "credit" && (!transaction.sender || (!transaction.sender.name && !transaction.sender.ename)) && - transaction.accountType === "group"; + transaction.accountType === "group" && + balance > 0; // Only consider it a mint if balance is positive + // Debit with no receiver and group account type is likely a burn const isLikelyBurn = transaction.type === "debit" && (!transaction.receiver || (!transaction.receiver.name && !transaction.receiver.ename)) && @@ -27,7 +40,9 @@ export default function TransactionCard({ transaction, currencyName, onClick }: const isMinted = hasMintInDesc || isLikelyMint; const isBurned = hasBurnInDesc || isLikelyBurn; - if (isMinted) { + if (isAccountInit) { + mainText = "Account Created"; + } else if (isMinted) { mainText = "Minted"; } else if (isBurned) { mainText = "Burned";