Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 8
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
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
b4e68a468440e0c4de3732899c8261938d1764c57086a1aa357c2763fce16b67cdf49dFile filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
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>{returnawaitAppDataSource.transaction("SERIALIZABLE",async(manager)=>{constledgerRepo=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 | ||
coderabbitai[bot] marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| ); | ||
| return debit; | ||
| } | ||
| async getTransactionHistory( | ||
| currencyId?: string, | ||
| accountId?: string, | ||
Uh oh!
There was an error while loading. Please reload this page.
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