From fbd38d2e5a7514ffd017c19353d3605cc5622f23 Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Mon, 23 Jun 2025 13:47:34 +0200 Subject: [PATCH] move decay into shared --- backend/src/graphql/model/Decay.ts | 10 +-- .../src/graphql/resolver/BalanceResolver.ts | 2 +- .../graphql/resolver/ContributionResolver.ts | 3 +- .../graphql/resolver/StatisticsResolver.ts | 2 +- .../resolver/TransactionLinkResolver.ts | 2 +- backend/src/logging/DecayLogging.view.ts | 3 +- backend/src/util/calculateSenderBalance.ts | 4 +- backend/src/util/decay.ts | 71 ------------------- backend/src/util/validate.ts | 4 +- backend/src/util/virtualTransactions.ts | 2 +- bun.lock | 1 + federation/src/config/schema.ts | 2 - federation/src/graphql/api/1_0/model/Decay.ts | 10 +-- .../api/1_0/util/calculateRecipientBalance.ts | 4 +- federation/src/graphql/util/decay.test.ts | 42 ----------- shared/package.json | 1 + shared/src/const/index.ts | 2 + shared/src/index.ts | 3 +- .../util => shared/src/logic}/decay.test.ts | 0 .../util => shared/src/logic}/decay.ts | 37 ++++++---- shared/src/logic/index.ts | 4 ++ 21 files changed, 49 insertions(+), 160 deletions(-) delete mode 100644 backend/src/util/decay.ts delete mode 100644 federation/src/graphql/util/decay.test.ts create mode 100644 shared/src/const/index.ts rename {backend/src/util => shared/src/logic}/decay.test.ts (100%) rename {federation/src/graphql/util => shared/src/logic}/decay.ts (57%) create mode 100644 shared/src/logic/index.ts diff --git a/backend/src/graphql/model/Decay.ts b/backend/src/graphql/model/Decay.ts index a32b96c13..838e442f7 100644 --- a/backend/src/graphql/model/Decay.ts +++ b/backend/src/graphql/model/Decay.ts @@ -1,14 +1,6 @@ import { Decimal } from 'decimal.js-light' import { Field, Int, ObjectType } from 'type-graphql' - -interface DecayInterface { - balance: Decimal - decay: Decimal - roundedDecay: Decimal - start: Date | null - end: Date | null - duration: number | null -} +import { Decay as DecayInterface } from 'shared' @ObjectType() export class Decay { diff --git a/backend/src/graphql/resolver/BalanceResolver.ts b/backend/src/graphql/resolver/BalanceResolver.ts index f3c7d4709..9ac6b125b 100644 --- a/backend/src/graphql/resolver/BalanceResolver.ts +++ b/backend/src/graphql/resolver/BalanceResolver.ts @@ -9,7 +9,7 @@ import { RIGHTS } from '@/auth/RIGHTS' import { BalanceLoggingView } from '@/logging/BalanceLogging.view' import { DecayLoggingView } from '@/logging/DecayLogging.view' import { Context, getUser } from '@/server/context' -import { calculateDecay } from '@/util/decay' +import { calculateDecay } from 'shared' import { getLogger } from 'log4js' import { LOG4JS_RESOLVER_CATEGORY_NAME } from '.' diff --git a/backend/src/graphql/resolver/ContributionResolver.ts b/backend/src/graphql/resolver/ContributionResolver.ts index 687aa0196..21162a4cb 100644 --- a/backend/src/graphql/resolver/ContributionResolver.ts +++ b/backend/src/graphql/resolver/ContributionResolver.ts @@ -19,7 +19,6 @@ import { ContributionType } from '@enum/ContributionType' import { TransactionTypeId } from '@enum/TransactionTypeId' import { AdminUpdateContribution } from '@model/AdminUpdateContribution' import { Contribution, ContributionListResult } from '@model/Contribution' -import { Decay } from '@model/Decay' import { OpenCreation } from '@model/OpenCreation' import { UnconfirmedContribution } from '@model/UnconfirmedContribution' @@ -44,7 +43,7 @@ import { UpdateUnconfirmedContributionContext } from '@/interactions/updateUncon import { LogError } from '@/server/LogError' import { Context, getClientTimezoneOffset, getUser } from '@/server/context' import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK' -import { calculateDecay } from '@/util/decay' +import { calculateDecay, Decay } from 'shared' import { fullName } from '@/util/utilities' import { LOG4JS_RESOLVER_CATEGORY_NAME } from '@/graphql/resolver' diff --git a/backend/src/graphql/resolver/StatisticsResolver.ts b/backend/src/graphql/resolver/StatisticsResolver.ts index 6713cbb54..7ba54fafb 100644 --- a/backend/src/graphql/resolver/StatisticsResolver.ts +++ b/backend/src/graphql/resolver/StatisticsResolver.ts @@ -5,7 +5,7 @@ import { Authorized, FieldResolver, Query, Resolver } from 'type-graphql' import { CommunityStatistics, DynamicStatisticsFields } from '@model/CommunityStatistics' import { RIGHTS } from '@/auth/RIGHTS' -import { calculateDecay } from '@/util/decay' +import { calculateDecay } from 'shared' const db = AppDatabase.getInstance() diff --git a/backend/src/graphql/resolver/TransactionLinkResolver.ts b/backend/src/graphql/resolver/TransactionLinkResolver.ts index 8d442b447..e75f79444 100644 --- a/backend/src/graphql/resolver/TransactionLinkResolver.ts +++ b/backend/src/graphql/resolver/TransactionLinkResolver.ts @@ -39,7 +39,7 @@ import { LogError } from '@/server/LogError' import { Context, getClientTimezoneOffset, getUser } from '@/server/context' import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK' import { TRANSACTION_LINK_LOCK } from '@/util/TRANSACTION_LINK_LOCK' -import { calculateDecay } from '@/util/decay' +import { calculateDecay } from 'shared' import { fullName } from '@/util/utilities' import { calculateBalance } from '@/util/validate' diff --git a/backend/src/logging/DecayLogging.view.ts b/backend/src/logging/DecayLogging.view.ts index f76f97821..713f069fa 100644 --- a/backend/src/logging/DecayLogging.view.ts +++ b/backend/src/logging/DecayLogging.view.ts @@ -1,9 +1,10 @@ import { AbstractLoggingView } from 'database' import { Decay } from '@/graphql/model/Decay' +import type { Decay as DecayType } from 'shared' export class DecayLoggingView extends AbstractLoggingView { - public constructor(private self: Decay) { + public constructor(private self: Decay | DecayType) { super() } diff --git a/backend/src/util/calculateSenderBalance.ts b/backend/src/util/calculateSenderBalance.ts index d2973c982..a01b1f15e 100644 --- a/backend/src/util/calculateSenderBalance.ts +++ b/backend/src/util/calculateSenderBalance.ts @@ -4,7 +4,7 @@ import { Decay } from '@model/Decay' import { getLastTransaction } from '@/graphql/resolver/util/getLastTransaction' -import { calculateDecay } from './decay' +import { calculateDecay } from 'shared' export async function calculateSenderBalance( userId: number, @@ -16,7 +16,7 @@ export async function calculateSenderBalance( return null } - const decay = calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, time) + const decay = new Decay(calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, time)) const balance = decay.balance.add(amount.toString()) return { balance, lastTransactionId: lastTransaction.id, decay } diff --git a/backend/src/util/decay.ts b/backend/src/util/decay.ts deleted file mode 100644 index 96c7ddb4b..000000000 --- a/backend/src/util/decay.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { Decimal } from 'decimal.js-light' - -import { Decay } from '@model/Decay' - -import { LogError } from '@/server/LogError' -import { DECAY_START_TIME } from 'config-schema' - -Decimal.set({ - precision: 25, - rounding: Decimal.ROUND_HALF_UP, -}) - -// TODO: externalize all those definitions and functions into an external decay library - -function decayFormula(value: Decimal, seconds: number): Decimal { - // TODO why do we need to convert this here to a stting to work properly? - return value.mul( - new Decimal('0.99999997803504048973201202316767079413460520837376').pow(seconds).toString(), - ) -} - -function calculateDecay( - amount: Decimal, - from: Date, - to: Date, - startBlock: Date = DECAY_START_TIME, -): Decay { - const fromMs = from.getTime() - const toMs = to.getTime() - const startBlockMs = startBlock.getTime() - - if (toMs < fromMs) { - throw new LogError('calculateDecay: to < from, reverse decay calculation is invalid') - } - - // Initialize with no decay - const decay: Decay = { - balance: amount, - decay: new Decimal(0), - roundedDecay: new Decimal(0), - start: null, - end: null, - duration: null, - } - - // decay started after end date; no decay - if (startBlockMs > toMs) { - return decay - } - // decay started before start date; decay for full duration - if (startBlockMs < fromMs) { - decay.start = from - decay.duration = (toMs - fromMs) / 1000 - } - // decay started between start and end date; decay from decay start till end date - else { - decay.start = startBlock - decay.duration = (toMs - startBlockMs) / 1000 - } - - decay.end = to - decay.balance = decayFormula(amount, decay.duration) - decay.decay = decay.balance.minus(amount) - decay.roundedDecay = amount - .toDecimalPlaces(2, Decimal.ROUND_DOWN) - .minus(decay.balance.toDecimalPlaces(2, Decimal.ROUND_DOWN).toString()) - .mul(-1) - return decay -} - -export { decayFormula, calculateDecay } diff --git a/backend/src/util/validate.ts b/backend/src/util/validate.ts index d0e493ff5..1dd8a4529 100644 --- a/backend/src/util/validate.ts +++ b/backend/src/util/validate.ts @@ -7,7 +7,7 @@ import { Decay } from '@model/Decay' import { getLastTransaction } from '@/graphql/resolver/util/getLastTransaction' import { transactionLinkSummary } from '@/graphql/resolver/util/transactionLinkSummary' -import { calculateDecay } from './decay' +import { calculateDecay } from 'shared' function isStringBoolean(value: string): boolean { const lowerValue = value.toLowerCase() @@ -36,7 +36,7 @@ async function calculateBalance( return null } - const decay = calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, time) + const decay = new Decay(calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, time)) const balance = decay.balance.add(amount.toString()) const { sumHoldAvailableAmount } = await transactionLinkSummary(userId, time) diff --git a/backend/src/util/virtualTransactions.ts b/backend/src/util/virtualTransactions.ts index b80642dfe..dabaee264 100644 --- a/backend/src/util/virtualTransactions.ts +++ b/backend/src/util/virtualTransactions.ts @@ -6,7 +6,7 @@ import { TransactionTypeId } from '@enum/TransactionTypeId' import { Transaction } from '@model/Transaction' import { User } from '@model/User' -import { calculateDecay } from './decay' +import { calculateDecay } from 'shared' const defaultModelFunctions = { hasId: function (): boolean { diff --git a/bun.lock b/bun.lock index a6b408a40..18cacf7bf 100644 --- a/bun.lock +++ b/bun.lock @@ -420,6 +420,7 @@ "name": "shared", "version": "2.6.0", "dependencies": { + "decimal.js-light": "^2.5.1", "esbuild": "^0.25.2", "log4js": "^6.9.1", "zod": "^3.25.61", diff --git a/federation/src/config/schema.ts b/federation/src/config/schema.ts index 82e05d3bc..07f5ed48f 100644 --- a/federation/src/config/schema.ts +++ b/federation/src/config/schema.ts @@ -1,5 +1,4 @@ import { - DECAY_START_TIME, GRAPHIQL, LOG4JS_CONFIG_PLACEHOLDER, LOG_FILES_BASE_PATH, @@ -10,7 +9,6 @@ import { import Joi from 'joi' export const schema = Joi.object({ - DECAY_START_TIME, GRAPHIQL, LOG4JS_CONFIG_PLACEHOLDER, LOG_FILES_BASE_PATH, diff --git a/federation/src/graphql/api/1_0/model/Decay.ts b/federation/src/graphql/api/1_0/model/Decay.ts index a32b96c13..f72fade3a 100644 --- a/federation/src/graphql/api/1_0/model/Decay.ts +++ b/federation/src/graphql/api/1_0/model/Decay.ts @@ -1,14 +1,6 @@ import { Decimal } from 'decimal.js-light' import { Field, Int, ObjectType } from 'type-graphql' - -interface DecayInterface { - balance: Decimal - decay: Decimal - roundedDecay: Decimal - start: Date | null - end: Date | null - duration: number | null -} +import { Decay as DecayInterface} from 'shared' @ObjectType() export class Decay { diff --git a/federation/src/graphql/api/1_0/util/calculateRecipientBalance.ts b/federation/src/graphql/api/1_0/util/calculateRecipientBalance.ts index 198c89289..ead2cc641 100644 --- a/federation/src/graphql/api/1_0/util/calculateRecipientBalance.ts +++ b/federation/src/graphql/api/1_0/util/calculateRecipientBalance.ts @@ -1,4 +1,4 @@ -import { calculateDecay } from '@/graphql/util/decay' +import { calculateDecay } from 'shared' import { getLastTransaction } from '@/graphql/util/getLastTransaction' import { Decimal } from 'decimal.js-light' import { Decay } from '../model/Decay' @@ -13,7 +13,7 @@ export async function calculateRecipientBalance( return null } - const decay = calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, time) + const decay = new Decay(calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, time)) const balance = decay.balance.add(amount.toString()) diff --git a/federation/src/graphql/util/decay.test.ts b/federation/src/graphql/util/decay.test.ts deleted file mode 100644 index f419982ac..000000000 --- a/federation/src/graphql/util/decay.test.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { Decimal } from 'decimal.js-light' - -import { calculateDecay, decayFormula } from './decay' - -describe('utils/decay', () => { - describe('decayFormula', () => { - it('has base 0.99999997802044727', () => { - const amount = new Decimal(1.0) - const seconds = 1 - // TODO: toString() was required, we could not compare two decimals - expect(decayFormula(amount, seconds).toString()).toBe('0.999999978035040489732012') - }) - it('has correct backward calculation', () => { - const amount = new Decimal(1.0) - const seconds = -1 - expect(decayFormula(amount, seconds).toString()).toBe('1.000000021964959992727444') - }) - // we get pretty close, but not exact here, skipping - - it.skip('has correct forward calculation', () => { - const amount = new Decimal(1.0).div( - new Decimal('0.99999997803504048973201202316767079413460520837376'), - ) - const seconds = 1 - expect(decayFormula(amount, seconds).toString()).toBe('1.0') - }) - }) - it('has base 0.99999997802044727', () => { - const now = new Date() - now.setSeconds(1) - const oneSecondAgo = new Date(now.getTime()) - oneSecondAgo.setSeconds(0) - expect(calculateDecay(new Decimal(1.0), oneSecondAgo, now).balance.toString()).toBe( - '0.999999978035040489732012', - ) - }) - - it('returns input amount when from and to is the same', () => { - const now = new Date() - expect(calculateDecay(new Decimal(100.0), now, now).balance.toString()).toBe('100') - }) -}) diff --git a/shared/package.json b/shared/package.json index 31c7404bc..31190ec7c 100644 --- a/shared/package.json +++ b/shared/package.json @@ -31,6 +31,7 @@ "uuid": "^8.3.2" }, "dependencies": { + "decimal.js-light": "^2.5.1", "esbuild": "^0.25.2", "log4js": "^6.9.1", "zod": "^3.25.61" diff --git a/shared/src/const/index.ts b/shared/src/const/index.ts new file mode 100644 index 000000000..6976b019a --- /dev/null +++ b/shared/src/const/index.ts @@ -0,0 +1,2 @@ +export const DECAY_START_TIME = new Date('2021-05-13T17:46:31Z') +export const LOG4JS_BASE_CATEGORY_NAME = 'shared' \ No newline at end of file diff --git a/shared/src/index.ts b/shared/src/index.ts index d252da56f..936131a69 100644 --- a/shared/src/index.ts +++ b/shared/src/index.ts @@ -1,2 +1,3 @@ export * from './schema' -export * from './enum' \ No newline at end of file +export * from './enum' +export * from './logic' diff --git a/backend/src/util/decay.test.ts b/shared/src/logic/decay.test.ts similarity index 100% rename from backend/src/util/decay.test.ts rename to shared/src/logic/decay.test.ts diff --git a/federation/src/graphql/util/decay.ts b/shared/src/logic/decay.ts similarity index 57% rename from federation/src/graphql/util/decay.ts rename to shared/src/logic/decay.ts index 9f2908b71..f75146515 100644 --- a/federation/src/graphql/util/decay.ts +++ b/shared/src/logic/decay.ts @@ -1,34 +1,47 @@ import { Decimal } from 'decimal.js-light' -import { LogError } from '@/server/LogError' -import { DECAY_START_TIME } from 'config-schema' -import { Decay } from '../api/1_0/model/Decay' +import { getLogger } from 'log4js' +import { LOG4JS_LOGIC_CATEGORY } from '.' +import { DECAY_START_TIME } from '../const' + +const logger = getLogger(`${LOG4JS_LOGIC_CATEGORY}.DecayLogic`) Decimal.set({ precision: 25, rounding: Decimal.ROUND_HALF_UP, }) -// TODO: externalize all those definitions and functions into an external decay library -function decayFormula(value: Decimal, seconds: number): Decimal { +export interface Decay { + balance: Decimal + decay: Decimal + roundedDecay: Decimal + start: Date | null + end: Date | null + duration: number | null +} + +export function decayFormula(value: Decimal, seconds: number): Decimal { // TODO why do we need to convert this here to a string to work properly? + // chatgpt: We convert to string here to avoid precision loss: + // .pow(seconds) can internally round the result, especially for large values of `seconds`. + // Using .toString() ensures full precision is preserved in the multiplication. return value.mul( new Decimal('0.99999997803504048973201202316767079413460520837376').pow(seconds).toString(), ) } -function calculateDecay( +export function calculateDecay( amount: Decimal, from: Date, - to: Date, - startBlock: Date = DECAY_START_TIME, + to: Date ): Decay { const fromMs = from.getTime() const toMs = to.getTime() - const startBlockMs = startBlock.getTime() + const startBlockMs = DECAY_START_TIME.getTime() if (toMs < fromMs) { - throw new LogError('calculateDecay: to < from, reverse decay calculation is invalid') + logger.error('calculateDecay: to < from, reverse decay calculation is invalid', from, to) + throw new Error('calculateDecay: to < from, reverse decay calculation is invalid') } // Initialize with no decay @@ -52,7 +65,7 @@ function calculateDecay( } // decay started between start and end date; decay from decay start till end date else { - decay.start = startBlock + decay.start = DECAY_START_TIME decay.duration = (toMs - startBlockMs) / 1000 } @@ -65,5 +78,3 @@ function calculateDecay( .mul(-1) return decay } - -export { decayFormula, calculateDecay } diff --git a/shared/src/logic/index.ts b/shared/src/logic/index.ts new file mode 100644 index 000000000..7056d5818 --- /dev/null +++ b/shared/src/logic/index.ts @@ -0,0 +1,4 @@ +import { LOG4JS_BASE_CATEGORY_NAME } from '../const' + +export const LOG4JS_LOGIC_CATEGORY = `${LOG4JS_BASE_CATEGORY_NAME}.logic` +export { calculateDecay, Decay } from './decay' \ No newline at end of file