mirror of
https://github.com/IT4Change/gradido.git
synced 2026-02-06 09:56:05 +00:00
1st implementation of voteForSendCoins
This commit is contained in:
parent
52160e19d6
commit
53a6f1fec6
@ -1,5 +1,5 @@
|
||||
import { Decimal } from 'decimal.js-light'
|
||||
import { ArgsType, Field, Int } from 'type-graphql'
|
||||
import { ArgsType, Field } from 'type-graphql'
|
||||
|
||||
@ArgsType()
|
||||
export class SendCoinsArgs {
|
||||
@ -9,6 +9,9 @@ export class SendCoinsArgs {
|
||||
@Field(() => String)
|
||||
userReceiverIdentifier: string
|
||||
|
||||
@Field(() => Date)
|
||||
creationDate: Date
|
||||
|
||||
@Field(() => Decimal)
|
||||
amount: Decimal
|
||||
|
||||
|
||||
@ -12,13 +12,13 @@ Decimal.set({
|
||||
|
||||
const constants = {
|
||||
DB_VERSION: '0071-add-pending_transactions-table',
|
||||
// DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0
|
||||
DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0
|
||||
LOG4JS_CONFIG: 'log4js-config.json',
|
||||
// default log level on production should be info
|
||||
LOG_LEVEL: process.env.LOG_LEVEL || 'info',
|
||||
CONFIG_VERSION: {
|
||||
DEFAULT: 'DEFAULT',
|
||||
EXPECTED: 'v1.2023-01-09',
|
||||
EXPECTED: 'v2.2023-08-24',
|
||||
CURRENT: '',
|
||||
},
|
||||
}
|
||||
|
||||
@ -0,0 +1,14 @@
|
||||
import { registerEnumType } from 'type-graphql'
|
||||
|
||||
export enum PendingTransactionState {
|
||||
NEW = 1,
|
||||
WAIT_ON_PENDING = 2,
|
||||
PENDING = 3,
|
||||
WAIT_ON_CONFIRM = 4,
|
||||
CONFIRMED = 5,
|
||||
}
|
||||
|
||||
registerEnumType(PendingTransactionState, {
|
||||
name: 'PendingTransactionState', // this one is mandatory
|
||||
description: 'State of the PendingTransaction', // this one is optional
|
||||
})
|
||||
15
federation/src/graphql/api/1_0/enum/TransactionTypeId.ts
Normal file
15
federation/src/graphql/api/1_0/enum/TransactionTypeId.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { registerEnumType } from 'type-graphql'
|
||||
|
||||
export enum TransactionTypeId {
|
||||
CREATION = 1,
|
||||
SEND = 2,
|
||||
RECEIVE = 3,
|
||||
// This is a virtual property, never occurring on the database
|
||||
DECAY = 4,
|
||||
LINK_SUMMARY = 5,
|
||||
}
|
||||
|
||||
registerEnumType(TransactionTypeId, {
|
||||
name: 'TransactionTypeId', // this one is mandatory
|
||||
description: 'Type of the transaction', // this one is optional
|
||||
})
|
||||
41
federation/src/graphql/api/1_0/model/Decay.ts
Normal file
41
federation/src/graphql/api/1_0/model/Decay.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import { Decimal } from 'decimal.js-light'
|
||||
import { ObjectType, Field, Int } from 'type-graphql'
|
||||
|
||||
interface DecayInterface {
|
||||
balance: Decimal
|
||||
decay: Decimal
|
||||
roundedDecay: Decimal
|
||||
start: Date | null
|
||||
end: Date | null
|
||||
duration: number | null
|
||||
}
|
||||
|
||||
@ObjectType()
|
||||
export class Decay {
|
||||
constructor({ balance, decay, roundedDecay, start, end, duration }: DecayInterface) {
|
||||
this.balance = balance
|
||||
this.decay = decay
|
||||
this.roundedDecay = roundedDecay
|
||||
this.start = start
|
||||
this.end = end
|
||||
this.duration = duration
|
||||
}
|
||||
|
||||
@Field(() => Decimal)
|
||||
balance: Decimal
|
||||
|
||||
@Field(() => Decimal)
|
||||
decay: Decimal
|
||||
|
||||
@Field(() => Decimal)
|
||||
roundedDecay: Decimal
|
||||
|
||||
@Field(() => Date, { nullable: true })
|
||||
start: Date | null
|
||||
|
||||
@Field(() => Date, { nullable: true })
|
||||
end: Date | null
|
||||
|
||||
@Field(() => Int, { nullable: true })
|
||||
duration: number | null
|
||||
}
|
||||
29
federation/src/graphql/api/1_0/model/SendCoinsArgs.ts
Normal file
29
federation/src/graphql/api/1_0/model/SendCoinsArgs.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { Decimal } from 'decimal.js-light'
|
||||
import { ArgsType, Field } from 'type-graphql'
|
||||
|
||||
@ArgsType()
|
||||
export class SendCoinsArgs {
|
||||
@Field(() => String)
|
||||
communityReceiverIdentifier: string
|
||||
|
||||
@Field(() => String)
|
||||
userReceiverIdentifier: string
|
||||
|
||||
@Field(() => Date)
|
||||
creationDate: Date
|
||||
|
||||
@Field(() => Decimal)
|
||||
amount: Decimal
|
||||
|
||||
@Field(() => String)
|
||||
memo: string
|
||||
|
||||
@Field(() => String)
|
||||
communitySenderIdentifier: string
|
||||
|
||||
@Field(() => String)
|
||||
userSenderIdentifier: string
|
||||
|
||||
@Field(() => String)
|
||||
userSenderName: string
|
||||
}
|
||||
66
federation/src/graphql/api/1_0/resolver/SendCoinsResolver.ts
Normal file
66
federation/src/graphql/api/1_0/resolver/SendCoinsResolver.ts
Normal file
@ -0,0 +1,66 @@
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
import { Mutation, Query, Resolver } from 'type-graphql'
|
||||
import { federationLogger as logger } from '@/server/logger'
|
||||
import { Community as DbCommunity } from '@entity/Community'
|
||||
import { PendingTransaction as DbPendingTransaction } from '@entity/PendingTransaction'
|
||||
import { SendCoinsArgs } from '../model/SendCoinsArgs'
|
||||
import { User as DbUser } from '@entity/User'
|
||||
import { LogError } from '@/server/LogError'
|
||||
import { PendingTransactionState } from '../enum/PendingTransactionState'
|
||||
import { TransactionTypeId } from '../enum/TransactionTypeId'
|
||||
import { calculateRecepientBalance } from '@/graphql/util/calculateRecepientBalance'
|
||||
import Decimal from 'decimal.js-light'
|
||||
import { fullName } from '@/graphql/util/fullName'
|
||||
|
||||
@Resolver()
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export class SendCoinsResolver {
|
||||
@Mutation(() => Boolean)
|
||||
async voteForSendCoins(args: SendCoinsArgs): Promise<boolean> {
|
||||
logger.debug(`voteForSendCoins() via apiVersion=1_0 ...`)
|
||||
try {
|
||||
// first check if receiver community is correct
|
||||
const homeCom = await DbCommunity.findOneBy({
|
||||
communityUuid: args.communityReceiverIdentifier,
|
||||
})
|
||||
if (!homeCom) {
|
||||
throw new LogError(`voteForSendCoins with wrong communityReceiverIdentifier`)
|
||||
}
|
||||
// second check if receiver user exists in this community
|
||||
const receiverUser = await DbUser.findOneBy({ gradidoID: args.userReceiverIdentifier })
|
||||
if (!receiverUser) {
|
||||
throw new LogError(
|
||||
`voteForSendCoins with unknown userReceiverIdentifier in the community=`,
|
||||
homeCom.name,
|
||||
)
|
||||
}
|
||||
const receiveBalance = await calculateRecepientBalance(
|
||||
receiverUser.id,
|
||||
args.amount,
|
||||
args.creationDate,
|
||||
)
|
||||
const pendingTx = DbPendingTransaction.create()
|
||||
pendingTx.amount = args.amount
|
||||
pendingTx.balance = receiveBalance ? receiveBalance.balance : new Decimal(0)
|
||||
pendingTx.balanceDate = args.creationDate
|
||||
pendingTx.decay = receiveBalance ? receiveBalance.decay.decay : new Decimal(0)
|
||||
pendingTx.decayStart = receiveBalance ? receiveBalance.decay.start : null
|
||||
pendingTx.linkedUserCommunityUuid = args.communitySenderIdentifier
|
||||
pendingTx.linkedUserGradidoID = args.userSenderIdentifier
|
||||
pendingTx.linkedUserName = args.userSenderName
|
||||
pendingTx.memo = args.memo
|
||||
pendingTx.previous = receiveBalance ? receiveBalance.lastTransactionId : null
|
||||
pendingTx.state = PendingTransactionState.NEW
|
||||
pendingTx.typeId = TransactionTypeId.RECEIVE
|
||||
pendingTx.userCommunityUuid = args.communityReceiverIdentifier
|
||||
pendingTx.userGradidoID = args.userReceiverIdentifier
|
||||
pendingTx.userName = fullName(receiverUser.firstName, receiverUser.lastName)
|
||||
|
||||
await DbPendingTransaction.insert(pendingTx)
|
||||
logger.debug(`voteForSendCoins()-1_0... successfull`)
|
||||
} catch (err) {
|
||||
throw new LogError(`Error in voteForSendCoins with args=`, args)
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
20
federation/src/graphql/util/calculateRecepientBalance.ts
Normal file
20
federation/src/graphql/util/calculateRecepientBalance.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { Decimal } from 'decimal.js-light'
|
||||
|
||||
import { getLastTransaction } from './getLastTransaction'
|
||||
import { calculateDecay } from './decay'
|
||||
import { Decay } from '../api/1_0/model/Decay'
|
||||
|
||||
export async function calculateRecepientBalance(
|
||||
userId: number,
|
||||
amount: Decimal,
|
||||
time: Date,
|
||||
): Promise<{ balance: Decimal; decay: Decay; lastTransactionId: number } | null> {
|
||||
const lastTransaction = await getLastTransaction(userId)
|
||||
if (!lastTransaction) return null
|
||||
|
||||
const decay = calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, time)
|
||||
|
||||
const balance = decay.balance.add(amount.toString())
|
||||
|
||||
return { balance, lastTransactionId: lastTransaction.id, decay }
|
||||
}
|
||||
42
federation/src/graphql/util/decay.test.ts
Normal file
42
federation/src/graphql/util/decay.test.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { Decimal } from 'decimal.js-light'
|
||||
|
||||
import { decayFormula, calculateDecay } 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
|
||||
// eslint-disable-next-line jest/no-disabled-tests
|
||||
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')
|
||||
})
|
||||
})
|
||||
65
federation/src/graphql/util/decay.ts
Normal file
65
federation/src/graphql/util/decay.ts
Normal file
@ -0,0 +1,65 @@
|
||||
import { Decimal } from 'decimal.js-light'
|
||||
|
||||
import { LogError } from '@/server/LogError'
|
||||
import CONFIG from '@/config'
|
||||
import { Decay } from '../api/1_0/model/Decay'
|
||||
|
||||
// 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 = CONFIG.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 }
|
||||
2
federation/src/graphql/util/fullName.ts
Normal file
2
federation/src/graphql/util/fullName.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export const fullName = (firstName: string, lastName: string): string =>
|
||||
[firstName, lastName].filter(Boolean).join(' ')
|
||||
12
federation/src/graphql/util/getLastTransaction.ts
Normal file
12
federation/src/graphql/util/getLastTransaction.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { Transaction as DbTransaction } from '@entity/Transaction'
|
||||
|
||||
export const getLastTransaction = async (
|
||||
userId: number,
|
||||
relations?: string[],
|
||||
): Promise<DbTransaction | null> => {
|
||||
return DbTransaction.findOne({
|
||||
where: { userId },
|
||||
order: { balanceDate: 'DESC', id: 'DESC' },
|
||||
relations,
|
||||
})
|
||||
}
|
||||
10
federation/src/server/LogError.ts
Normal file
10
federation/src/server/LogError.ts
Normal file
@ -0,0 +1,10 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||
import { federationLogger as logger } from './logger'
|
||||
|
||||
export class LogError extends Error {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
constructor(msg: string, ...details: any[]) {
|
||||
super(msg)
|
||||
logger.error(msg, ...details)
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user