-
Notifications
You must be signed in to change notification settings - Fork 5
feat: add burn button and auto reload #623
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
b4e68a4
68440e0
c4de373
2899c82
61938d1
764c570
86a1aa3
57c2763
fce16b6
7cdf49d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| import { MigrationInterface, QueryRunner } from "typeorm"; | ||
|
|
||
| export class Migration1765809758435 implements MigrationInterface { | ||
| name = 'Migration1765809758435' | ||
|
|
||
| public async up(queryRunner: QueryRunner): Promise<void> { | ||
| await queryRunner.query(`ALTER TABLE "ledger" ADD "hash" text`); | ||
| await queryRunner.query(`ALTER TABLE "ledger" ADD "prevHash" text`); | ||
| } | ||
|
|
||
| public async down(queryRunner: QueryRunner): Promise<void> { | ||
| await queryRunner.query(`ALTER TABLE "ledger" DROP COLUMN "prevHash"`); | ||
| await queryRunner.query(`ALTER TABLE "ledger" DROP COLUMN "hash"`); | ||
| } | ||
|
|
||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,6 +5,7 @@ import { Currency } from "../database/entities/Currency"; | |
| import { User } from "../database/entities/User"; | ||
| import { Group } from "../database/entities/Group"; | ||
| import { TransactionNotificationService } from "./TransactionNotificationService"; | ||
| import crypto from "crypto"; | ||
|
|
||
| export class LedgerService { | ||
| ledgerRepository: Repository<Ledger>; | ||
|
|
@@ -15,6 +16,18 @@ export class LedgerService { | |
| this.currencyRepository = AppDataSource.getRepository(Currency); | ||
| } | ||
|
|
||
| private computeHash(payload: any): string { | ||
| return crypto.createHash("sha256").update(JSON.stringify(payload)).digest("hex"); | ||
| } | ||
|
|
||
| private async getPrevHash(currencyId: string): Promise<string | null> { | ||
| const prev = await this.ledgerRepository.findOne({ | ||
| where: { currencyId }, | ||
| order: { createdAt: "DESC", id: "DESC" }, | ||
| }); | ||
| return prev?.hash ?? null; | ||
| } | ||
|
Comment on lines
+23
to
+29
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Race condition risk in hash chain creation. When multiple concurrent Consider wrapping the entire private async getPrevHash(currencyId: string): Promise<string | null> {
- const prev = await this.ledgerRepository.findOne({
+ // NOTE: This should be called within a serializable transaction
+ // to prevent race conditions in hash chain construction
+ const prev = await this.ledgerRepository.findOne({
where: { currencyId },
order: { createdAt: "DESC", id: "DESC" },
});
return prev?.hash ?? null;
}For a proper fix, the async addLedgerEntry(...): Promise<Ledger> {
return await AppDataSource.transaction("SERIALIZABLE", async (manager) => {
const ledgerRepo = manager.getRepository(Ledger);
// ... perform all operations within this transaction
});
}🤖 Prompt for AI Agents |
||
|
|
||
| async getAccountBalance(currencyId: string, accountId: string, accountType: AccountType): Promise<number> { | ||
| const latestEntry = await this.ledgerRepository.findOne({ | ||
| where: { | ||
|
|
@@ -67,10 +80,34 @@ export class LedgerService { | |
| balance: newBalance, | ||
| }; | ||
|
|
||
| const entry = this.ledgerRepository.create(entryData); | ||
| // capture previous hash before inserting current entry | ||
| const prevHash = await this.getPrevHash(currencyId); | ||
|
|
||
| const entry = this.ledgerRepository.create(entryData); | ||
| const saved = await this.ledgerRepository.save(entry); | ||
| return Array.isArray(saved) ? saved[0] : saved; | ||
|
|
||
| const hashPayload = { | ||
| id: saved.id, | ||
| currencyId: saved.currencyId, | ||
| accountId: saved.accountId, | ||
| accountType: saved.accountType, | ||
| amount: saved.amount, | ||
| type: saved.type, | ||
| description: saved.description, | ||
| senderAccountId: saved.senderAccountId, | ||
| senderAccountType: saved.senderAccountType, | ||
| receiverAccountId: saved.receiverAccountId, | ||
| receiverAccountType: saved.receiverAccountType, | ||
| balance: saved.balance, | ||
| createdAt: saved.createdAt, | ||
| prevHash, | ||
| }; | ||
|
|
||
| saved.prevHash = prevHash; | ||
| saved.hash = this.computeHash(hashPayload); | ||
|
|
||
| const finalized = await this.ledgerRepository.save(saved); | ||
| return Array.isArray(finalized) ? finalized[0] : finalized; | ||
|
Comment on lines
+86
to
+110
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Two-phase save lacks atomicity; entry may persist without hash on failure. The current flow saves the entry first (line 87), then computes and saves the hash (line 109). If the second save fails (e.g., network issue, DB constraint), the ledger entry persists without Wrap both operations in a single transaction to ensure atomicity: - const entry = this.ledgerRepository.create(entryData);
- const saved = await this.ledgerRepository.save(entry);
-
- const hashPayload = { ... };
-
- saved.prevHash = prevHash;
- saved.hash = this.computeHash(hashPayload);
-
- const finalized = await this.ledgerRepository.save(saved);
- return Array.isArray(finalized) ? finalized[0] : finalized;
+ return await AppDataSource.transaction(async (manager) => {
+ const repo = manager.getRepository(Ledger);
+ const entry = repo.create(entryData);
+ const saved = await repo.save(entry);
+
+ const hashPayload = { /* ... fields ... */ };
+ saved.prevHash = prevHash;
+ saved.hash = this.computeHash(hashPayload);
+
+ return await repo.save(saved);
+ });Additionally, line 110's
|
||
| } | ||
|
|
||
| async transfer( | ||
|
|
@@ -160,6 +197,54 @@ export class LedgerService { | |
| return { debit, credit }; | ||
| } | ||
|
|
||
| async burn( | ||
| currencyId: string, | ||
| groupId: string, | ||
| amount: number, | ||
| description?: string | ||
| ): Promise<Ledger> { | ||
| 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, | ||
| undefined, | ||
| undefined, | ||
| currentBalance | ||
| ); | ||
|
|
||
| return debit; | ||
| } | ||
|
|
||
| async getTransactionHistory( | ||
| currencyId?: string, | ||
| accountId?: string, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: MetaState-Prototype-Project/prototype
Length of output: 138
🏁 Script executed:
Repository: MetaState-Prototype-Project/prototype
Length of output: 139
🏁 Script executed:
Repository: MetaState-Prototype-Project/prototype
Length of output: 557
🏁 Script executed:
Repository: MetaState-Prototype-Project/prototype
Length of output: 1705
🏁 Script executed:
Repository: MetaState-Prototype-Project/prototype
Length of output: 63
🏁 Script executed:
Repository: MetaState-Prototype-Project/prototype
Length of output: 794
🏁 Script executed:
Repository: MetaState-Prototype-Project/prototype
Length of output: 45571
🏁 Script executed:
Repository: MetaState-Prototype-Project/prototype
Length of output: 114
🏁 Script executed:
Repository: MetaState-Prototype-Project/prototype
Length of output: 854
🏁 Script executed:
Repository: MetaState-Prototype-Project/prototype
Length of output: 637
🏁 Script executed:
Repository: MetaState-Prototype-Project/prototype
Length of output: 603
🏁 Script executed:
Repository: MetaState-Prototype-Project/prototype
Length of output: 14862
🏁 Script executed:
Repository: MetaState-Prototype-Project/prototype
Length of output: 455
🏁 Script executed:
Repository: MetaState-Prototype-Project/prototype
Length of output: 4061
🏁 Script executed:
Repository: MetaState-Prototype-Project/prototype
Length of output: 131
🏁 Script executed:
Repository: MetaState-Prototype-Project/prototype
Length of output: 2134
🏁 Script executed:
Repository: MetaState-Prototype-Project/prototype
Length of output: 4530
🏁 Script executed:
Repository: MetaState-Prototype-Project/prototype
Length of output: 187
🏁 Script executed:
Repository: MetaState-Prototype-Project/prototype
Length of output: 330
🏁 Script executed:
# Also check if there's an update to any migration or initial migration that includes these fields git log --oneline -20 -- platforms/eCurrency-api/src/database/Repository: MetaState-Prototype-Project/prototype
Length of output: 114
🏁 Script executed:
Repository: MetaState-Prototype-Project/prototype
Length of output: 230
🏁 Script executed:
Repository: MetaState-Prototype-Project/prototype
Length of output: 455
🏁 Script executed:
Repository: MetaState-Prototype-Project/prototype
Length of output: 3102
🏁 Script executed:
Repository: MetaState-Prototype-Project/prototype
Length of output: 117
🏁 Script executed:
Repository: MetaState-Prototype-Project/prototype
Length of output: 2151
🏁 Script executed:
Repository: MetaState-Prototype-Project/prototype
Length of output: 5242
🏁 Script executed:
Repository: MetaState-Prototype-Project/prototype
Length of output: 63
🏁 Script executed:
Repository: MetaState-Prototype-Project/prototype
Length of output: 63
🏁 Script executed:
Repository: MetaState-Prototype-Project/prototype
Length of output: 279
Database migration for hash/prevHash columns is missing—application will fail at runtime.
The Ledger entity defines
hashandprevHashcolumns and the LedgerService writes to these fields, but the latest database migration (1765784749012-migration.ts) does NOT include ALTER TABLE statements to add these columns. When the application attempts to save a ledger entry with these new fields, it will crash with a "column does not exist" error.A migration file must be created immediately to add the nullable text columns to the ledger table before deploying this feature:
🤖 Prompt for AI Agents