/* MIGRATION TO FIX WRONG BALANCE * * Due to a bug in the code * the amount of a receive balance is substracted * from the previous balance instead of added. * * Therefore all balance and decay fields must * be recalculated * * WARNING: This Migration must be run in TZ=UTC */ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ /* eslint-disable @typescript-eslint/no-explicit-any */ import fs from 'fs' import Decimal from 'decimal.js-light' // Set precision value Decimal.set({ precision: 25, rounding: Decimal.ROUND_HALF_UP, }) const DECAY_START_TIME = new Date('2021-05-13 17:46:31') // GMT+0 interface Decay { balance: Decimal decay: Decimal | null start: Date | null end: Date | null duration: number | null } export enum TransactionTypeId { CREATION = 1, SEND = 2, RECEIVE = 3, } function decayFormula(value: Decimal, seconds: number): Decimal { return value.mul(new Decimal('0.99999997803504048973201202316767079413460520837376').pow(seconds)) } 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 Error('to < from, reverse decay calculation is invalid') } // Initialize with no decay const decay: Decay = { balance: amount, decay: null, 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) return decay } export async function upgrade(queryFn: (query: string, values?: any[]) => Promise>) { // Write log file const logFile = 'log/0054-recalculate_balance_and_decay.log.csv' await fs.writeFile( logFile, `email;first_name;last_name;affected_transactions;new_balance;new_decay;old_balance;old_decay;delta;\n`, (err) => { if (err) throw err }, ) // Find all users & loop over them const users = await queryFn('SELECT user_id FROM transactions GROUP BY user_id;') for (let u = 0; u < users.length; u++) { const userId = users[u].user_id // find all transactions for a user const transactions = await queryFn( `SELECT *, CONVERT(balance, CHAR) as dec_balance, CONVERT(decay, CHAR) as dec_decay FROM transactions WHERE user_id = ${userId} ORDER BY balance_date ASC;`, ) let previous = null let affectedTransactions = 0 let balance = new Decimal(0) for (let t = 0; t < transactions.length; t++) { const transaction = transactions[t] const decayStartDate = previous ? previous.balance_date : transaction.balance_date const amount = new Decimal(transaction.amount) const decay = calculateDecay(balance, decayStartDate, transaction.balance_date) balance = decay.balance.add(amount) const userContact = await queryFn( `SELECT email, first_name, last_name FROM users LEFT JOIN user_contacts ON users.email_id = user_contacts.id WHERE users.id = ${userId}`, ) const userEmail = userContact.length === 1 ? userContact[0].email : userId const userFirstName = userContact.length === 1 ? userContact[0].first_name : '' const userLastName = userContact.length === 1 ? userContact[0].last_name : '' // Update if needed if (!balance.eq(transaction.dec_balance)) { await queryFn(` UPDATE transactions SET balance = ${balance}, decay = ${decay.decay ? decay.decay : 0} WHERE id = ${transaction.id}; `) affectedTransactions++ // Log on last entry if (t === transactions.length - 1) { fs.appendFile( logFile, `${userEmail};${userFirstName};${userLastName};${affectedTransactions};${balance};${ decay.decay ? decay.decay : 0 };${transaction.dec_balance};${transaction.dec_decay};${balance.sub( transaction.dec_balance, )};\n`, (err) => { if (err) throw err }, ) } } // previous previous = transaction } } } /* eslint-disable @typescript-eslint/no-empty-function */ /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ export async function downgrade(queryFn: (query: string, values?: any[]) => Promise>) {}