refactor, make it easier to read

This commit is contained in:
einhornimmond 2025-12-23 15:33:42 +01:00
parent 5c753a3b32
commit 02483a5993
31 changed files with 1515 additions and 413 deletions

View File

@ -68,6 +68,16 @@ export async function upgrade(queryFn: (query: string, values?: any[]) => Promis
AND t.user_id <> u.id
;`)
await queryFn(`
UPDATE contributions c
JOIN users u ON(c.moderator_id = u.id)
SET c.confirmed_by = u.id
WHERE c.contribution_status = 'CONFIRMED'
AND c.user_id = c.confirmed_by
AND u.created_at < c.confirmed_at
AND c.user_id <> u.id
;`)
/**
* Fix 2: Replace invalid moderators with the earliest ADMIN.
*
@ -112,6 +122,25 @@ export async function upgrade(queryFn: (query: string, values?: any[]) => Promis
AND t.user_id <> moderator.id
;`)
// similar but with confirmed by user
await queryFn(`
UPDATE contributions c
JOIN (
SELECT c_sub.id as sub_c_id, u_sub.created_at, u_sub.id
FROM contributions c_sub
JOIN users u_sub ON (c_sub.confirmed_by <> u_sub.id AND c_sub.user_id <> u_sub.id)
JOIN user_roles r_sub ON (u_sub.id = r_sub.user_id)
WHERE r_sub.role IN ('ADMIN', 'MODERATOR')
GROUP BY c_sub.id
ORDER BY r_sub.created_at ASC
) confirmingUser ON (c.id = confirmingUser.sub_c_id)
LEFT JOIN users u on(c.confirmed_by = u.id)
SET c.confirmed_by = confirmingUser.id
WHERE c.confirmed_at <= u.created_at
AND c.confirmed_at > confirmingUser.created_at
AND c.user_id <> confirmingUser.id
;`)
/**
* Fix 3: Update user creation dates to ensure historical consistency.
*
@ -174,6 +203,26 @@ export async function upgrade(queryFn: (query: string, values?: any[]) => Promis
WHERE CHAR_LENGTH(t.memo) < 5
;`)
await queryFn(`
UPDATE contributions t
SET t.memo = CASE
WHEN CHAR_LENGTH(t.memo) = 0 THEN 'empty empty'
WHEN CHAR_LENGTH(t.memo) < 5 THEN LPAD(t.memo, 5, ' ')
ELSE t.memo
END
WHERE CHAR_LENGTH(t.memo) < 5
;`)
await queryFn(`
UPDATE transaction_links t
SET t.memo = CASE
WHEN CHAR_LENGTH(t.memo) = 0 THEN 'empty empty'
WHEN CHAR_LENGTH(t.memo) < 5 THEN LPAD(t.memo, 5, ' ')
ELSE t.memo
END
WHERE CHAR_LENGTH(t.memo) < 5
;`)
/**
* Fix 5: Insert missing 'ADMIN_CONTRIBUTION_LINK_CREATE' events for contribution_links.
*

View File

@ -76,4 +76,17 @@ export class KeyPairCacheManager {
}
return keyPair
}
public getKeyPairSync(
input: string,
createKeyPair: () => KeyPairEd25519,
): KeyPairEd25519 {
const keyPair = this.cache.get(input)
if (!keyPair) {
const keyPair = createKeyPair()
this.cache.set(input, keyPair)
return keyPair
}
return keyPair
}
}

View File

@ -0,0 +1,45 @@
import { KeyPairEd25519, MemoryBlock } from 'gradido-blockchain-js'
import { GradidoBlockchainCryptoError, ParameterError } from '../errors'
import { Hex32, Uuidv4 } from '../schemas/typeGuard.schema'
import { hardenDerivationIndex } from '../utils/derivationHelper'
export function deriveFromSeed(seed: Hex32): KeyPairEd25519 {
const keyPair = KeyPairEd25519.create(MemoryBlock.fromHex(seed))
if (!keyPair) {
throw new Error(`couldn't create keyPair from seed: ${seed}`)
}
return keyPair
}
export function deriveFromCode(code: string): KeyPairEd25519 {
// code is expected to be 24 bytes long, but we need 32
// so hash the seed with blake2 and we have 32 Bytes
const hash = new MemoryBlock(code).calculateHash()
const keyPair = KeyPairEd25519.create(hash)
if (!keyPair) {
throw new ParameterError(
`error creating Ed25519 KeyPair from seed: ${code.substring(0, 5)}...`,
)
}
return keyPair
}
export function deriveFromKeyPairAndUuid(keyPair: KeyPairEd25519, uuid: Uuidv4): KeyPairEd25519 {
const wholeHex = Buffer.from(uuid.replace(/-/g, ''), 'hex')
const parts = []
for (let i = 0; i < 4; i++) {
parts[i] = hardenDerivationIndex(wholeHex.subarray(i * 4, (i + 1) * 4).readUInt32BE())
}
// parts: [2206563009, 2629978174, 2324817329, 2405141782]
return parts.reduce((keyPair: KeyPairEd25519, node: number) => deriveFromKeyPairAndIndex(keyPair, node), keyPair)
}
export function deriveFromKeyPairAndIndex(keyPair: KeyPairEd25519, index: number): KeyPairEd25519 {
const localKeyPair = keyPair.deriveChild(index)
if (!localKeyPair) {
throw new GradidoBlockchainCryptoError(
`KeyPairEd25519 child derivation failed, has private key: ${keyPair.hasPrivateKey()}, index: ${index}`,
)
}
return localKeyPair
}

View File

@ -11,7 +11,7 @@ import { LOG4JS_BASE_CATEGORY } from '../../config/const'
import { Uuidv4 } from '../../schemas/typeGuard.schema'
import { loadUserByGradidoId } from './database'
import { bytesToMbyte } from './utils'
import { CommunityContext, CreatedUserDb } from './valibot.schema'
import { CommunityContext, UserDb } from './valibot.schema'
dotenv.config()
@ -20,10 +20,10 @@ export class Context {
public db: MySql2Database
public communities: Map<string, CommunityContext>
public cache: KeyPairCacheManager
public balanceFixGradidoUser: CreatedUserDb | null
public balanceFixGradidoUser: UserDb | null
private timeUsed: Profiler
constructor(logger: Logger, db: MySql2Database, cache: KeyPairCacheManager, balanceFixGradidoUser: CreatedUserDb | null) {
constructor(logger: Logger, db: MySql2Database, cache: KeyPairCacheManager, balanceFixGradidoUser: UserDb | null) {
this.logger = logger
this.db = db
this.cache = cache
@ -44,7 +44,7 @@ export class Context {
})
const db = drizzle({ client: connection })
const logger = getLogger(`${LOG4JS_BASE_CATEGORY}.migrations.db-v2.7.0_to_blockchain-v3.5`)
let balanceFixGradidoUser: CreatedUserDb | null = null
let balanceFixGradidoUser: UserDb | null = null
if (process.env.MIGRATION_ACCOUNT_BALANCE_FIX_GRADIDO_ID) {
balanceFixGradidoUser = await loadUserByGradidoId(db, process.env.MIGRATION_ACCOUNT_BALANCE_FIX_GRADIDO_ID)
if (!balanceFixGradidoUser) {

View File

@ -32,7 +32,7 @@ let transactionAddedToBlockchainSum = 0
let addToBlockchainSum = 0
const sizeBuffer = Buffer.alloc(2)
function addToBlockchain(
export function addToBlockchain(
builder: GradidoTransactionBuilder,
blockchain: InMemoryBlockchain,
transactionId: number,
@ -53,7 +53,7 @@ function addToBlockchain(
sizeBuffer.writeUInt16LE(binTransaction.size(), 0)
fs.appendFileSync(filePath, sizeBuffer)
fs.appendFileSync(filePath, binTransaction.data())
//
//*/
try {
const result = blockchain.createAndAddConfirmedTransactionExtern(

View File

@ -1,17 +1,15 @@
import { AccountBalance, AccountBalances, GradidoUnit, InMemoryBlockchainProvider } from 'gradido-blockchain-js'
import { randomBytes } from 'node:crypto'
import { AccountBalances, GradidoTransactionBuilder, InMemoryBlockchainProvider } from 'gradido-blockchain-js'
import * as v from 'valibot'
import { KeyPairIdentifierLogic } from '../../data/KeyPairIdentifier.logic'
import { GradidoBlockchainCryptoError } from '../../errors'
import { ResolveKeyPair } from '../../interactions/resolveKeyPair/ResolveKeyPair.context'
import { HieroId, hieroIdSchema } from '../../schemas/typeGuard.schema'
import { CONFIG } from '../../config'
import { deriveFromSeed } from '../../data/deriveKeyPair'
import { Hex32, hex32Schema } from '../../schemas/typeGuard.schema'
import { AUF_ACCOUNT_DERIVATION_INDEX, GMW_ACCOUNT_DERIVATION_INDEX, hardenDerivationIndex } from '../../utils/derivationHelper'
import { addCommunityRootTransaction } from './blockchain'
import { addToBlockchain } from './blockchain'
import { Context } from './Context'
import { communityDbToCommunity } from './convert'
import { loadAdminUsersCache, loadCommunities, loadContributionLinkModeratorCache } from './database'
import { generateKeyPairCommunity } from './data/keyPair'
import { CommunityContext } from './valibot.schema'
import { Balance } from './data/Balance'
import { loadAdminUsersCache, loadCommunities, loadContributionLinkModeratorCache } from './database'
import { CommunityContext } from './valibot.schema'
export async function bootstrap(): Promise<Context> {
const context = await Context.create()
@ -26,62 +24,63 @@ export async function bootstrap(): Promise<Context> {
async function bootstrapCommunities(context: Context): Promise<Map<string, CommunityContext>> {
const communities = new Map<string, CommunityContext>()
const communitiesDb = await loadCommunities(context.db)
const topicIds = new Set<HieroId>()
const communityNames = new Set<string>()
for (const communityDb of communitiesDb) {
let alias = communityDb.name
if (communityNames.has(communityDb.name)) {
alias = communityDb.communityUuid
} else {
communityNames.add(communityDb.name)
}
const blockchain = InMemoryBlockchainProvider.getInstance().findBlockchain(
communityDb.uniqueAlias,
alias,
)
if (!blockchain) {
throw new Error(`Couldn't create Blockchain for community ${communityDb.communityUuid}`)
throw new Error(`Couldn't create Blockchain for community ${alias}`)
}
context.logger.info(`Blockchain for community '${communityDb.uniqueAlias}' created`)
// make sure topic id is unique
let topicId: HieroId
do {
topicId = v.parse(hieroIdSchema, '0.0.' + Math.floor(Math.random() * 10000))
} while (topicIds.has(topicId))
topicIds.add(topicId)
communities.set(communityDb.communityUuid, {
communityId: communityDb.uniqueAlias,
blockchain,
topicId,
folder: communityDb.uniqueAlias.replace(/[^a-zA-Z0-9]/g, '_'),
gmwBalance: new Balance(),
aufBalance: new Balance(),
})
generateKeyPairCommunity(communityDb, context.cache, topicId)
context.logger.info(`Blockchain for community '${alias}' created`)
let seed: Hex32
if (!communityDb.foreign) {
seed = v.parse(hex32Schema, CONFIG.HOME_COMMUNITY_SEED.convertToHex())
} else {
seed = v.parse(hex32Schema, randomBytes(32).toString('hex'))
}
let creationDate = communityDb.creationDate
if (communityDb.userMinCreatedAt && communityDb.userMinCreatedAt < communityDb.creationDate) {
// create community root transaction 1 minute before first user
creationDate = new Date(new Date(communityDb.userMinCreatedAt).getTime() - 1000 * 60)
}
// community from db to community format the dlt connector normally uses
const community = communityDbToCommunity(topicId, communityDb, creationDate)
// TODO: remove code for gmw and auf key somewhere else
const communityKeyPair = await ResolveKeyPair(new KeyPairIdentifierLogic({ communityTopicId: topicId }))
const gmwKeyPair = communityKeyPair.deriveChild(
hardenDerivationIndex(GMW_ACCOUNT_DERIVATION_INDEX),
)
if (!gmwKeyPair) {
throw new GradidoBlockchainCryptoError(
`KeyPairEd25519 child derivation failed, has private key: ${communityKeyPair.hasPrivateKey()} for community: ${communityDb.communityUuid}`,
)
const communityKeyPair = deriveFromSeed(seed)
const gmwKeyPair = communityKeyPair.deriveChild(hardenDerivationIndex(GMW_ACCOUNT_DERIVATION_INDEX))
const aufKeyPair = communityKeyPair.deriveChild(hardenDerivationIndex(AUF_ACCOUNT_DERIVATION_INDEX))
if (!communityKeyPair || !gmwKeyPair || !aufKeyPair) {
throw new Error(`Error on creating key pair for community ${JSON.stringify(communityDb, null, 2)}`)
}
const aufKeyPair = communityKeyPair.deriveChild(
hardenDerivationIndex(AUF_ACCOUNT_DERIVATION_INDEX),
)
if (!aufKeyPair) {
throw new GradidoBlockchainCryptoError(
`KeyPairEd25519 child derivation failed, has private key: ${communityKeyPair.hasPrivateKey()} for community: ${communityDb.communityUuid}`,
const builder = new GradidoTransactionBuilder()
builder
.setCreatedAt(creationDate)
.setCommunityRoot(
communityKeyPair.getPublicKey(),
gmwKeyPair.getPublicKey(),
aufKeyPair.getPublicKey(),
)
.sign(communityKeyPair)
const communityContext: CommunityContext = {
communityId: alias,
blockchain,
keyPair: communityKeyPair,
folder: alias.replace(/[^a-zA-Z0-9]/g, '_'),
gmwBalance: new Balance(gmwKeyPair.getPublicKey()!),
aufBalance: new Balance(aufKeyPair.getPublicKey()!),
}
communities.set(communityDb.communityUuid, communityContext)
const accountBalances = new AccountBalances()
accountBalances.add(new AccountBalance(gmwKeyPair.getPublicKey(), GradidoUnit.zero(), ''))
accountBalances.add(new AccountBalance(aufKeyPair.getPublicKey(), GradidoUnit.zero(), ''))
await addCommunityRootTransaction(blockchain, community, accountBalances)
accountBalances.add(communityContext.aufBalance.getAccountBalance())
accountBalances.add(communityContext.gmwBalance.getAccountBalance())
addToBlockchain(builder, blockchain, communityDb.id, accountBalances)
}
return communities
}

View File

@ -1,25 +1,63 @@
import Decimal from 'decimal.js-light'
import { GradidoUnit } from 'gradido-blockchain-js'
import { AccountBalance, GradidoUnit, MemoryBlockPtr } from 'gradido-blockchain-js'
import { legacyCalculateDecay } from '../utils'
export class Balance {
private balance: GradidoUnit
private date: Date
public constructor()
{
private publicKey: MemoryBlockPtr
private communityId?: string | null
constructor(publicKey: MemoryBlockPtr, communityId?: string | null) {
this.balance = new GradidoUnit(0)
this.date = new Date()
this.publicKey = publicKey
this.communityId = communityId
}
public update(amount: Decimal, date: Date) {
static fromAccountBalance(accountBalance: AccountBalance, confirmedAt: Date): Balance {
const balance = new Balance(accountBalance.getPublicKey()!, accountBalance.getCommunityId() || null)
balance.update(accountBalance.getBalance(), confirmedAt)
return balance
}
getBalance(): GradidoUnit {
return this.balance
}
updateLegacyDecay(amount: GradidoUnit, date: Date) {
const previousBalanceString = this.balance.toString()
if (this.balance.equal(GradidoUnit.zero())) {
this.balance = GradidoUnit.fromString(amount.toString())
this.balance = amount
this.date = date
} else {
const decayedBalance = legacyCalculateDecay(new Decimal(this.balance.toString()), this.date, date )
const newBalance = decayedBalance.add(amount)
const newBalance = decayedBalance.add(new Decimal(amount.toString()))
this.balance = GradidoUnit.fromString(newBalance.toString())
this.date = date
}
if (this.balance.lt(GradidoUnit.zero())) {
throw new Error(`negative Gradido amount detected in Balance.updateLegacyDecay, previous balance: ${previousBalanceString}, amount: ${amount.toString()}`)
}
}
update(amount: GradidoUnit, date: Date) {
const previousBalanceString = this.balance.toString()
if (this.balance.equal(GradidoUnit.zero())) {
this.balance = amount
this.date = date
} else {
this.balance = this.balance
.calculateDecay(this.date, date)
.add(amount)
this.date = date
}
if (this.balance.lt(GradidoUnit.zero())) {
throw new Error(`negative Gradido amount detected in Balance.update, previous balance: ${previousBalanceString}, amount: ${amount.toString()}`)
}
}
getAccountBalance(): AccountBalance {
return new AccountBalance(this.publicKey, this.balance, this.communityId || '')
}
}

View File

@ -0,0 +1,7 @@
export enum ContributionStatus {
PENDING = 'PENDING',
DELETED = 'DELETED',
IN_PROGRESS = 'IN_PROGRESS',
DENIED = 'DENIED',
CONFIRMED = 'CONFIRMED',
}

View File

@ -4,6 +4,8 @@ import { MySql2Database } from 'drizzle-orm/mysql2'
import { getLogger } from 'log4js'
import * as v from 'valibot'
import { LOG4JS_BASE_CATEGORY } from '../../config/const'
import { ContributionStatus } from './data/ContributionStatus'
import { TransactionTypeId } from './data/TransactionTypeId'
import {
communitiesTable,
contributionsTable,
@ -17,12 +19,14 @@ import {
userSelectSchema,
usersTable
} from './drizzle.schema'
import { TransactionTypeId } from './data/TransactionTypeId'
import { DatabaseError } from './errors'
import {
CommunityDb,
CreatedUserDb,
UserDb,
CreationTransactionDb,
communityDbSchema,
createdUserDbSchema,
userDbSchema,
creationTransactionDbSchema,
TransactionDb,
TransactionLinkDb,
transactionDbSchema,
@ -33,8 +37,8 @@ const logger = getLogger(
`${LOG4JS_BASE_CATEGORY}.migrations.db-v2.7.0_to_blockchain-v3.6.blockchain`,
)
export const contributionLinkModerators = new Map<number, CreatedUserDb>()
export const adminUsers = new Map<string, CreatedUserDb>()
export const contributionLinkModerators = new Map<number, UserDb>()
export const adminUsers = new Map<string, UserDb>()
const transactionIdSet = new Set<number>()
export async function loadContributionLinkModeratorCache(db: MySql2Database): Promise<void> {
@ -49,7 +53,7 @@ export async function loadContributionLinkModeratorCache(db: MySql2Database): Pr
.orderBy(asc(eventsTable.id))
result.map((row: any) => {
contributionLinkModerators.set(row.event.involvedContributionLinkId, v.parse(createdUserDbSchema, row.user))
contributionLinkModerators.set(row.event.involvedContributionLinkId, v.parse(userDbSchema, row.user))
})
}
@ -63,7 +67,7 @@ export async function loadAdminUsersCache(db: MySql2Database): Promise<void> {
.leftJoin(usersTable, eq(userRolesTable.userId, usersTable.id))
result.map((row: any) => {
adminUsers.set(row.gradidoId, v.parse(createdUserDbSchema, row.user))
adminUsers.set(row.gradidoId, v.parse(userDbSchema, row.user))
})
}
@ -71,6 +75,7 @@ export async function loadAdminUsersCache(db: MySql2Database): Promise<void> {
export async function loadCommunities(db: MySql2Database): Promise<CommunityDb[]> {
const result = await db
.select({
id: communitiesTable.id,
foreign: communitiesTable.foreign,
communityUuid: communitiesTable.communityUuid,
name: communitiesTable.name,
@ -83,18 +88,8 @@ export async function loadCommunities(db: MySql2Database): Promise<CommunityDb[]
.orderBy(asc(communitiesTable.id))
.groupBy(communitiesTable.communityUuid)
const communityNames = new Set<string>()
return result.map((row: any) => {
let alias = row.name
if (communityNames.has(row.name)) {
alias = row.community_uuid
} else {
communityNames.add(row.name)
}
return v.parse(communityDbSchema, {
...row,
uniqueAlias: alias,
})
return v.parse(communityDbSchema, row)
})
}
@ -102,7 +97,7 @@ export async function loadUsers(
db: MySql2Database,
offset: number,
count: number,
): Promise<CreatedUserDb[]> {
): Promise<UserDb[]> {
const result = await db
.select()
.from(usersTable)
@ -110,19 +105,59 @@ export async function loadUsers(
.limit(count)
.offset(offset)
return result.map((row: any) => {
return v.parse(createdUserDbSchema, row)
})
return result.map((row: any) => v.parse(userDbSchema, row))
}
export async function loadUserByGradidoId(db: MySql2Database, gradidoId: string): Promise<CreatedUserDb | null> {
export async function loadUserByGradidoId(db: MySql2Database, gradidoId: string): Promise<UserDb | null> {
const result = await db
.select()
.from(usersTable)
.where(eq(usersTable.gradidoId, gradidoId))
.limit(1)
return result.length ? v.parse(createdUserDbSchema, result[0]) : null
return result.length ? v.parse(userDbSchema, result[0]) : null
}
export async function loadLocalTransferTransactions(
db: MySql2Database,
offset: number,
count: number,
): Promise<TransactionDb[]> {
const linkedUsers = alias(usersTable, 'linkedUser')
const result = await db
.select({
transaction: transactionsTable,
user: usersTable,
linkedUser: linkedUsers,
})
.from(transactionsTable)
.where(
and(
eq(transactionsTable.typeId, TransactionTypeId.RECEIVE),
isNull(transactionsTable.transactionLinkId),
isNotNull(transactionsTable.linkedUserId),
eq(usersTable.foreign, 0),
eq(linkedUsers.foreign, 0),
)
)
.leftJoin(usersTable, eq(transactionsTable.userId, usersTable.id))
.leftJoin(linkedUsers, eq(transactionsTable.linkedUserId, linkedUsers.id))
.orderBy(asc(transactionsTable.balanceDate), asc(transactionsTable.id))
.limit(count)
.offset(offset)
return result.map((row: any) => {
const item = {
...row.transaction,
user: row.user,
linkedUser: row.linkedUser,
}
try {
return v.parse(transactionDbSchema, item)
} catch (e) {
throw new DatabaseError('loadLocalTransferTransactions', item, e as Error)
}
})
}
export async function loadTransactions(
@ -131,8 +166,7 @@ export async function loadTransactions(
count: number,
): Promise<TransactionDb[]> {
const linkedUsers = alias(usersTable, 'linkedUser')
const linkedTransactions = alias(transactionsTable, 'linkedTransaction')
const result = await db
.select({
transaction: transactionsTable,
@ -142,7 +176,6 @@ export async function loadTransactions(
id: transactionLinksTable.id,
code: transactionLinksTable.code
},
linkedUserBalance: linkedTransactions.balance,
})
.from(transactionsTable)
.where(
@ -158,7 +191,6 @@ export async function loadTransactions(
transactionLinksTable,
eq(transactionsTable.transactionLinkId, transactionLinksTable.id),
)
.leftJoin(linkedTransactions, eq(transactionsTable.linkedTransactionId, linkedTransactions.id))
.orderBy(asc(transactionsTable.balanceDate), asc(transactionsTable.id))
.limit(count)
.offset(offset)
@ -176,7 +208,6 @@ export async function loadTransactions(
transactionLinkCode: row.transactionLink ? row.transactionLink.code : null,
user: row.user,
linkedUser: row.linkedUser,
linkedUserBalance: row.linkedUserBalance,
})
} catch (e) {
logger.error(`table row: ${JSON.stringify(row, null, 2)}`)
@ -188,6 +219,43 @@ export async function loadTransactions(
})
}
export async function loadCreations(
db: MySql2Database,
offset: number,
count: number,
): Promise<CreationTransactionDb[]> {
const confirmedByUsers = alias(usersTable, 'confirmedByUser')
const result = await db
.select({
contribution: contributionsTable,
user: usersTable,
confirmedByUser: confirmedByUsers,
})
.from(contributionsTable)
.where(and(
isNull(contributionsTable.contributionLinkId),
eq(contributionsTable.contributionStatus, ContributionStatus.CONFIRMED),
))
.leftJoin(usersTable, eq(contributionsTable.userId, usersTable.id))
.leftJoin(confirmedByUsers, eq(contributionsTable.confirmedBy, confirmedByUsers.id))
.orderBy(asc(contributionsTable.confirmedAt), asc(contributionsTable.id))
.limit(count)
.offset(offset)
return result.map((row) => {
const creationTransactionDb = {
...row.contribution,
user: row.user,
confirmedByUser: row.confirmedByUser,
}
try {
return v.parse(creationTransactionDbSchema, creationTransactionDb)
} catch (e) {
throw new DatabaseError('loadCreations', creationTransactionDb, e as Error)
}
})
}
export async function loadInvalidContributionTransactions(
db: MySql2Database,
offset: number,

View File

@ -28,10 +28,14 @@ export const communitiesTable = mysqlTable(
export const contributionsTable = mysqlTable('contributions', {
id: int().autoincrement().notNull(),
userId: int('user_id').default(sql`NULL`),
contributionDate: datetime("contribution_date", { mode: 'string'}).default(sql`NULL`),
memo: varchar({ length: 512 }).notNull(),
amount: decimal({ precision: 40, scale: 20 }).notNull(),
contributionLinkId: int('contribution_link_id').default(sql`NULL`),
confirmedBy: int('confirmed_by').default(sql`NULL`),
confirmedAt: datetime('confirmed_at', { mode: 'string'}).default(sql`NULL`),
deletedAt: datetime('deleted_at', { mode: 'string'}).default(sql`NULL`),
contributionStatus: varchar('contribution_status', { length: 12 }).default('\'PENDING\'').notNull(),
transactionId: int('transaction_id').default(sql`NULL`),
})
@ -49,9 +53,11 @@ export const usersTable = mysqlTable(
foreign: tinyint().default(0).notNull(),
gradidoId: char('gradido_id', { length: 36 }).notNull(),
communityUuid: varchar('community_uuid', { length: 36 }).default(sql`NULL`),
deletedAt: datetime('deleted_at', { mode: 'string', fsp: 3 }).default(sql`NULL`),
createdAt: datetime('created_at', { mode: 'string', fsp: 3 })
.default(sql`current_timestamp(3)`)
.notNull(),
},
(table) => [unique('uuid_key').on(table.gradidoId, table.communityUuid)],
)
@ -100,4 +106,6 @@ export const transactionLinksTable = mysqlTable('transaction_links', {
createdAt: datetime({ mode: 'string' }).notNull(),
deletedAt: datetime({ mode: 'string' }).default(sql`NULL`),
validUntil: datetime({ mode: 'string' }).notNull(),
redeemedAt: datetime({ mode: 'string'}).default(sql`NULL`),
redeemedBy: int().default(sql`NULL`),
})

View File

@ -1,6 +1,44 @@
import * as v from 'valibot'
export class NotEnoughGradidoBalanceError extends Error {
constructor(public needed: number, public exist: number) {
super(`Not enough Gradido Balance for send coins, needed: ${needed} Gradido, exist: ${exist} Gradido`)
}
}
export class DatabaseError extends Error {
constructor(message: string, rows: any, originalError: Error) {
const parts: string[] = [`DatabaseError in ${message}`]
// Valibot-specific
if (originalError instanceof v.ValiError) {
const flattened = v.flatten(originalError.issues)
parts.push('Validation errors:')
parts.push(JSON.stringify(flattened, null, 2))
} else {
parts.push(`Original error: ${originalError.message}`)
}
parts.push('Rows:')
parts.push(JSON.stringify(rows, null, 2))
super(parts.join('\n\n'))
this.name = 'DatabaseError'
}
}
export class BlockchainError extends Error {
constructor(message: string, item: any, originalError: Error) {
const parts: string[] = [`BlockchainError in ${message}`]
parts.push(`Original error: ${originalError.message}`)
parts.push('Item:')
parts.push(JSON.stringify(item, null, 2))
super(parts.join('\n\n'))
this.name = 'BlockchainError'
this.stack = originalError.stack
}
}

View File

@ -1,10 +1,11 @@
import { Filter } from 'gradido-blockchain-js'
import * as v from 'valibot'
import { onShutdown } from '../../../../shared/src/helper/onShutdown'
import { uuidv4Schema } from '../../schemas/typeGuard.schema'
import { exportAllCommunities } from './binaryExport'
import { bootstrap } from './bootstrap'
import { syncDbWithBlockchainContext } from './interaction/syncDbWithBlockchain/syncDbWithBlockchain.context'
const BATCH_SIZE = 1000
const BATCH_SIZE = 500
async function main() {
// prepare in memory blockchains
@ -17,7 +18,12 @@ async function main() {
})
// synchronize to in memory blockchain
await syncDbWithBlockchainContext(context, BATCH_SIZE)
try {
await syncDbWithBlockchainContext(context, BATCH_SIZE)
} catch(e) {
console.error(e)
//context.logBlogchain(v.parse(uuidv4Schema, 'e70da33e-5976-4767-bade-aa4e4fa1c01a'))
}
// write as binary file for GradidoNode
exportAllCommunities(context, BATCH_SIZE)

View File

@ -1,10 +1,38 @@
import { AccountBalances } from 'gradido-blockchain-js'
import { AccountBalances, Filter, InMemoryBlockchain, SearchDirection_DESC } from 'gradido-blockchain-js'
import { KeyPairIdentifierLogic } from '../../../../data/KeyPairIdentifier.logic'
import { ResolveKeyPair } from '../../../../interactions/resolveKeyPair/ResolveKeyPair.context'
import { IdentifierAccount } from '../../../../schemas/account.schema'
import { Transaction } from '../../../../schemas/transaction.schema'
import { Context } from '../../Context'
import { Balance } from '../../data/Balance'
export class AbstractBalancesRole {
public accountBalances: AccountBalances
export abstract class AbstractBalancesRole {
public constructor(protected transaction: Transaction) {}
public constructor() {
this.accountBalances = new AccountBalances()
abstract getAccountBalances(context: Context): Promise<AccountBalances>
async getLastBalanceForUser(identifierAccount: IdentifierAccount, blockchain: InMemoryBlockchain): Promise<Balance> {
const userKeyPair = await ResolveKeyPair(
new KeyPairIdentifierLogic(identifierAccount),
)
const f = new Filter()
f.involvedPublicKey = userKeyPair.getPublicKey()
f.pagination.size = 1
f.searchDirection = SearchDirection_DESC
const lastSenderTransaction = blockchain.findOne(f)
if (!lastSenderTransaction) {
throw new Error(`no last transaction found for user: ${JSON.stringify(identifierAccount, null, 2)}`)
}
const lastConfirmedTransaction = lastSenderTransaction.getConfirmedTransaction()
if (!lastConfirmedTransaction) {
throw new Error(`invalid transaction, getConfirmedTransaction call failed for transaction nr: ${lastSenderTransaction.getTransactionNr()}`)
}
const senderLastAccountBalance = lastConfirmedTransaction.getAccountBalance(userKeyPair.getPublicKey(), '')
if (!senderLastAccountBalance) {
throw new Error(
`no sender account balance found for transaction nr: ${lastSenderTransaction.getTransactionNr()} and public key: ${userKeyPair.getPublicKey()?.convertToHex()}`
)
}
return Balance.fromAccountBalance(senderLastAccountBalance, lastConfirmedTransaction.getConfirmedAt().getDate())
}
}

View File

@ -1,5 +1,33 @@
import { AccountBalances } from 'gradido-blockchain-js'
import { Transaction } from '../../../../schemas/transaction.schema'
import { Context } from '../../Context'
import { TransactionDb } from '../../valibot.schema'
import { AbstractBalancesRole } from './AbstractBalances.role'
export class CreationBalancesRole extends AbstractBalancesRole {
}
constructor(transaction: Transaction, protected dbTransaction: TransactionDb) {
super(transaction)
}
async getAccountBalances(context: Context): Promise<AccountBalances> {
if (this.dbTransaction.linkedUser.communityUuid !== this.dbTransaction.user.communityUuid) {
throw new Error('creation: both recipient and signer must belong to same community')
}
const accountBalances = new AccountBalances()
const communityContext = context.getCommunityContextByUuid(this.dbTransaction.user.communityUuid)
const balance = await this.getLastBalanceForUser(this.transaction.user, communityContext.blockchain)
// calculate decay since last balance with legacy calculation method
balance.updateLegacyDecay(this.dbTransaction.amount, this.dbTransaction.balanceDate)
communityContext.aufBalance.updateLegacyDecay(this.dbTransaction.amount, this.dbTransaction.balanceDate)
communityContext.gmwBalance.updateLegacyDecay(this.dbTransaction.amount, this.dbTransaction.balanceDate)
accountBalances.add(balance.getAccountBalance())
accountBalances.add(communityContext.aufBalance.getAccountBalance())
accountBalances.add(communityContext.gmwBalance.getAccountBalance())
return accountBalances
}
}

View File

@ -0,0 +1,33 @@
import { AccountBalance, AccountBalances, GradidoUnit } from 'gradido-blockchain-js'
import { KeyPairIdentifierLogic } from '../../../../data/KeyPairIdentifier.logic'
import { ResolveKeyPair } from '../../../../interactions/resolveKeyPair/ResolveKeyPair.context'
import { Transaction } from '../../../../schemas/transaction.schema'
import { Context } from '../../Context'
import { TransactionLinkDb } from '../../valibot.schema'
import { AbstractBalancesRole } from './AbstractBalances.role'
export class DeferredTransferBalancesRole extends AbstractBalancesRole {
constructor(transaction: Transaction, protected dbTransactionLink: TransactionLinkDb) {
super(transaction)
}
async getAccountBalances(context: Context): Promise<AccountBalances> {
const senderCommunityContext = context.getCommunityContextByUuid(this.dbTransactionLink.user.communityUuid)
const accountBalances = new AccountBalances()
const seededIdentifier = new KeyPairIdentifierLogic(this.transaction.linkedUser!)
if (!seededIdentifier.isSeedKeyPair()) {
throw new Error(`linked user is not a seed: ${JSON.stringify(this.transaction, null, 2)}`)
}
const seedKeyPair = await ResolveKeyPair(seededIdentifier)
const senderAccountBalance = await this.getLastBalanceForUser(this.transaction.user, senderCommunityContext.blockchain)
let amount = GradidoUnit.fromString(this.dbTransactionLink.amount.toString())
amount = amount.calculateCompoundInterest((this.dbTransactionLink.validUntil.getTime() - this.dbTransactionLink.createdAt.getTime()) / 60000)
senderAccountBalance.updateLegacyDecay(amount.negated(), this.dbTransactionLink.createdAt)
accountBalances.add(senderAccountBalance.getAccountBalance())
accountBalances.add(new AccountBalance(seedKeyPair.getPublicKey(), amount, ''))
return accountBalances
}
}

View File

@ -0,0 +1,30 @@
import { AccountBalances } from 'gradido-blockchain-js'
import { Transaction } from '../../../../schemas/transaction.schema'
import { Context } from '../../Context'
import { TransactionDb } from '../../valibot.schema'
import { AbstractBalancesRole } from './AbstractBalances.role'
export class RedeemDeferredTransferBalancesRole extends AbstractBalancesRole {
constructor(transaction: Transaction, protected dbTransaction: TransactionDb) {
super(transaction)
}
async getAccountBalances(context: Context): Promise<AccountBalances> {
// I use the receiving part of transaction pair, so the user is the recipient and the linked user the sender and amount is positive
const senderCommunityContext = context.getCommunityContextByUuid(this.dbTransaction.linkedUser.communityUuid)
const recipientCommunityContext = context.getCommunityContextByUuid(this.dbTransaction.user.communityUuid)
const accountBalances = new AccountBalances()
context.cache.setHomeCommunityTopicId(senderCommunityContext.topicId)
const senderLastBalance = await this.getLastBalanceForUser(this.transaction.linkedUser!, senderCommunityContext.blockchain)
context.cache.setHomeCommunityTopicId(recipientCommunityContext.topicId)
const recipientLastBalance = await this.getLastBalanceForUser(this.transaction.user, recipientCommunityContext.blockchain)
senderLastBalance.updateLegacyDecay(this.dbTransaction.amount.negate(), this.dbTransaction.balanceDate)
recipientLastBalance.updateLegacyDecay(this.dbTransaction.amount, this.dbTransaction.balanceDate)
accountBalances.add(senderLastBalance.getAccountBalance())
accountBalances.add(recipientLastBalance.getAccountBalance())
return accountBalances
}
}

View File

@ -0,0 +1,17 @@
import { AccountBalance, AccountBalances, GradidoUnit } from 'gradido-blockchain-js'
import { KeyPairIdentifierLogic } from '../../../../data/KeyPairIdentifier.logic'
import { ResolveKeyPair } from '../../../../interactions/resolveKeyPair/ResolveKeyPair.context'
import { Context } from '../../Context'
import { AbstractBalancesRole } from './AbstractBalances.role'
export class RegisterAddressBalancesRole extends AbstractBalancesRole {
async getAccountBalances(_context: Context): Promise<AccountBalances> {
const accountBalances = new AccountBalances()
const recipientKeyPair = await ResolveKeyPair(
new KeyPairIdentifierLogic(this.transaction.user),
)
accountBalances.add(new AccountBalance(recipientKeyPair.getPublicKey(), GradidoUnit.zero(), ''))
return accountBalances
}
}

View File

@ -0,0 +1,30 @@
import { AccountBalances } from 'gradido-blockchain-js'
import { Transaction } from '../../../../schemas/transaction.schema'
import { Context } from '../../Context'
import { TransactionDb } from '../../valibot.schema'
import { AbstractBalancesRole } from './AbstractBalances.role'
export class TransferBalancesRole extends AbstractBalancesRole {
constructor(transaction: Transaction, protected dbTransaction: TransactionDb) {
super(transaction)
}
async getAccountBalances(context: Context): Promise<AccountBalances> {
// I use the receiving part of transaction pair, so the user is the recipient and the linked user the sender and amount is positive
const senderCommunityContext = context.getCommunityContextByUuid(this.dbTransaction.linkedUser.communityUuid)
const recipientCommunityContext = context.getCommunityContextByUuid(this.dbTransaction.user.communityUuid)
const accountBalances = new AccountBalances()
context.cache.setHomeCommunityTopicId(senderCommunityContext.topicId)
const senderLastBalance = await this.getLastBalanceForUser(this.transaction.linkedUser!, senderCommunityContext.blockchain)
context.cache.setHomeCommunityTopicId(recipientCommunityContext.topicId)
const recipientLastBalance = await this.getLastBalanceForUser(this.transaction.user, recipientCommunityContext.blockchain)
senderLastBalance.updateLegacyDecay(this.dbTransaction.amount.negate(), this.dbTransaction.balanceDate)
recipientLastBalance.updateLegacyDecay(this.dbTransaction.amount, this.dbTransaction.balanceDate)
accountBalances.add(senderLastBalance.getAccountBalance())
accountBalances.add(recipientLastBalance.getAccountBalance())
return accountBalances
}
}

View File

@ -1,70 +1,32 @@
import { AccountBalances } from 'gradido-blockchain-js'
import * as v from 'valibot'
import { InputTransactionType } from '../../../../data/InputTransactionType.enum'
import { Transaction } from '../../../../schemas/transaction.schema'
import { Context } from '../../Context'
import { TransactionDb } from '../../valibot.schema'
import { TransactionDb, TransactionLinkDb, transactionDbSchema, transactionLinkDbSchema } from '../../valibot.schema'
import { AbstractBalancesRole } from './AbstractBalances.role'
import { CreationBalancesRole } from './CreationBalances.role'
import { DeferredTransferBalancesRole } from './DeferredTransferBalances.role'
import { RedeemDeferredTransferBalancesRole } from './RedeemDeferredTransferBalances.role'
import { RegisterAddressBalancesRole } from './RegisterAddressBalances.role'
import { TransferBalancesRole } from './TransferBalances.role'
export function accountBalancesContext(transaction: Transaction, dbTransaction: TransactionDb, context: Context) {
let role: AbstractBalancesRole | null = null
if (InputTransactionType.GRADIDO_CREATION === transaction.type) {
role = new CreationBalancesRole()
}
export async function accountBalancesContext(transaction: Transaction, item: TransactionDb | TransactionLinkDb, context: Context): Promise<AccountBalances> {
let role: AbstractBalancesRole | null = null
if (InputTransactionType.GRADIDO_CREATION === transaction.type) {
role = new CreationBalancesRole(transaction, v.parse(transactionDbSchema, item))
} else if (InputTransactionType.GRADIDO_TRANSFER === transaction.type) {
role = new TransferBalancesRole(transaction, v.parse(transactionDbSchema, item))
} else if (InputTransactionType.GRADIDO_DEFERRED_TRANSFER === transaction.type) {
role = new DeferredTransferBalancesRole(transaction, v.parse(transactionLinkDbSchema, item))
} else if (InputTransactionType.GRADIDO_REDEEM_DEFERRED_TRANSFER === transaction.type) {
role = new RedeemDeferredTransferBalancesRole(transaction, v.parse(transactionDbSchema, item))
} else if (InputTransactionType.REGISTER_ADDRESS === transaction.type) {
role = new RegisterAddressBalancesRole(transaction)
}
if (!role) {
throw new Error(`No role found for transaction type ${transaction.type}`)
}
return await role.getAccountBalances(context)
}
/*
const senderCommunityContext = this.context.getCommunityContextByUuid(item.user.communityUuid)
const recipientCommunityContext = this.context.getCommunityContextByUuid(
item.linkedUser.communityUuid,
)
this.context.cache.setHomeCommunityTopicId(senderCommunityContext.topicId)
const transaction = transactionDbToTransaction(
item,
senderCommunityContext.topicId,
recipientCommunityContext.topicId,
)
const accountBalances = new AccountBalances()
if (InputTransactionType.GRADIDO_CREATION === transaction.type) {
const recipientKeyPair = await ResolveKeyPair(
new KeyPairIdentifierLogic(transaction.linkedUser!),
)
accountBalances.add(new AccountBalance(recipientKeyPair.getPublicKey(), item.balance, ''))
// update gmw and auf
this.updateGmwAuf(new Decimal(item.amount.toString(4)), item.balanceDate)
const communityKeyPair = await ResolveKeyPair(new KeyPairIdentifierLogic({ communityTopicId: senderCommunityContext.topicId }))
const gmwKeyPair = communityKeyPair.deriveChild(
hardenDerivationIndex(GMW_ACCOUNT_DERIVATION_INDEX),
)
if (!gmwKeyPair) {
throw new GradidoBlockchainCryptoError(
`KeyPairEd25519 child derivation failed, has private key: ${communityKeyPair.hasPrivateKey()} for community: ${senderCommunityContext.communityId}`,
)
}
const aufKeyPair = communityKeyPair.deriveChild(
hardenDerivationIndex(AUF_ACCOUNT_DERIVATION_INDEX),
)
if (!aufKeyPair) {
throw new GradidoBlockchainCryptoError(
`KeyPairEd25519 child derivation failed, has private key: ${communityKeyPair.hasPrivateKey()} for community: ${senderCommunityContext.communityId}`,
)
}
accountBalances.add(new AccountBalance(gmwKeyPair.getPublicKey(), GradidoUnit.fromString(
TransactionsSyncRole.gmwBalance!.balance.toString()), ''))
accountBalances.add(new AccountBalance(aufKeyPair.getPublicKey(), GradidoUnit.fromString(
TransactionsSyncRole.aufBalance!.balance.toString()), ''))
} else if (InputTransactionType.REGISTER_ADDRESS === transaction.type) {
const recipientKeyPair = await ResolveKeyPair(
new KeyPairIdentifierLogic(transaction.user),
)
accountBalances.add(new AccountBalance(recipientKeyPair.getPublicKey(), GradidoUnit.zero(), ''))
} else {
// I use the receiving part of transaction pair, so the user is the recipient and the linked user the sender
const senderKeyPair = await ResolveKeyPair(
new KeyPairIdentifierLogic(transaction.linkedUser!),
)
const recipientKeyPair = await ResolveKeyPair(
new KeyPairIdentifierLogic(transaction.user),
)
accountBalances.add(new AccountBalance(senderKeyPair.getPublicKey(), item.linkedUserBalance, ''))
accountBalances.add(new AccountBalance(recipientKeyPair.getPublicKey(), item.balance, ''))
*/

View File

@ -1,7 +1,11 @@
import { Profiler } from 'gradido-blockchain-js'
import { Filter, InMemoryBlockchain, KeyPairEd25519, MemoryBlockPtr, Profiler, SearchDirection_DESC } from 'gradido-blockchain-js'
import { getLogger, Logger } from 'log4js'
import { LOG4JS_BASE_CATEGORY } from '../../../../config/const'
import { deriveFromKeyPairAndIndex, deriveFromKeyPairAndUuid } from '../../../../data/deriveKeyPair'
import { Uuidv4 } from '../../../../schemas/typeGuard.schema'
import { Context } from '../../Context'
import { Balance } from '../../data/Balance'
import { CommunityContext } from '../../valibot.schema'
export abstract class AbstractSyncRole<T> {
private items: T[] = []
@ -14,9 +18,34 @@ export abstract class AbstractSyncRole<T> {
)
}
getAccountKeyPair(communityContext: CommunityContext, gradidoId: Uuidv4): KeyPairEd25519 {
return this.context.cache.getKeyPairSync(gradidoId, () => {
return deriveFromKeyPairAndIndex(deriveFromKeyPairAndUuid(communityContext.keyPair, gradidoId), 1)
})
}
getLastBalanceForUser(publicKey: MemoryBlockPtr, blockchain: InMemoryBlockchain, communityId: string = ''): Balance {
if (publicKey.isEmpty()) {
throw new Error('publicKey is empty')
}
const lastSenderTransaction = blockchain.findOne(Filter.lastBalanceFor(publicKey))
if (!lastSenderTransaction) {
return new Balance(publicKey, communityId)
}
const lastConfirmedTransaction = lastSenderTransaction.getConfirmedTransaction()
if (!lastConfirmedTransaction) {
throw new Error(`invalid transaction, getConfirmedTransaction call failed for transaction nr: ${lastSenderTransaction.getTransactionNr()}`)
}
const senderLastAccountBalance = lastConfirmedTransaction.getAccountBalance(publicKey, communityId)
if (!senderLastAccountBalance) {
return new Balance(publicKey, communityId)
}
return Balance.fromAccountBalance(senderLastAccountBalance, lastConfirmedTransaction.getConfirmedAt().getDate())
}
abstract getDate(): Date
abstract loadFromDb(offset: number, count: number): Promise<T[]>
abstract pushToBlockchain(item: T): Promise<void>
abstract pushToBlockchain(item: T): void
abstract itemTypeName(): string
// return count of new loaded items
@ -38,11 +67,11 @@ export abstract class AbstractSyncRole<T> {
return 0
}
async toBlockchain(): Promise<void> {
toBlockchain(): void {
if (this.isEmpty()) {
throw new Error(`[toBlockchain] No items, please call this only if isEmpty returns false`)
}
await this.pushToBlockchain(this.shift())
this.pushToBlockchain(this.shift())
}
peek(): T {

View File

@ -1,10 +1,14 @@
import { and, asc, eq, isNotNull } from 'drizzle-orm'
import * as v from 'valibot'
import { Context } from '../../Context'
import { adminUsers, contributionLinkModerators, loadContributionLinkTransactions } from '../../database'
import { CreatedUserDb, TransactionDb, transactionDbSchema } from '../../valibot.schema'
import { TransactionsSyncRole } from './TransactionsSync.role'
import { contributionLinkModerators } from '../../database'
import { CreationTransactionDb, creationTransactionDbSchema } from '../../valibot.schema'
import { CreationsSyncRole } from './CreationsSync.role'
import { contributionsTable, usersTable } from '../../drizzle.schema'
import { ContributionStatus } from '../../data/ContributionStatus'
import { DatabaseError } from '../../errors'
export class ContributionLinkTransactionSyncRole extends TransactionsSyncRole {
export class ContributionLinkTransactionSyncRole extends CreationsSyncRole {
constructor(readonly context: Context) {
super(context)
}
@ -12,24 +16,42 @@ export class ContributionLinkTransactionSyncRole extends TransactionsSyncRole {
return 'contributionLinkTransaction'
}
async loadFromDb(offset: number, count: number): Promise<TransactionDb[]> {
const transactionUsers = await loadContributionLinkTransactions(this.context.db, offset, count)
return transactionUsers.map((transactionUser) => {
let linkedUser: CreatedUserDb | null | undefined = null
linkedUser = contributionLinkModerators.get(transactionUser.contributionLinkId)
if (linkedUser?.gradidoId === transactionUser.user.gradidoId) {
for (const adminUser of adminUsers.values()) {
if (adminUser.gradidoId !== transactionUser.user.gradidoId) {
linkedUser = adminUser
break
}
}
}
return v.parse(transactionDbSchema, {
...transactionUser.transaction,
user: transactionUser.user,
linkedUser,
async loadFromDb(offset: number, count: number): Promise<CreationTransactionDb[]> {
const result = await this.context.db
.select({
contribution: contributionsTable,
user: usersTable,
})
})
.from(contributionsTable)
.where(and(
isNotNull(contributionsTable.contributionLinkId),
eq(contributionsTable.contributionStatus, ContributionStatus.CONFIRMED),
))
.innerJoin(usersTable, eq(contributionsTable.userId, usersTable.id))
.orderBy(asc(contributionsTable.confirmedAt), asc(contributionsTable.transactionId))
.limit(count)
.offset(offset)
const verifiedCreationTransactions: CreationTransactionDb[] = []
for(const row of result) {
if (!row.contribution.contributionLinkId) {
throw new Error(`expect contributionLinkId to be set: ${JSON.stringify(row.contribution, null, 2)}`)
}
const item = {
...row.contribution,
user: row.user,
confirmedByUser: contributionLinkModerators.get(row.contribution.contributionLinkId)
}
if (!item.confirmedByUser || item.userId === item.confirmedByUser.id) {
this.context.logger.warn(`skipped Contribution Link Transaction ${row.contribution.id}`)
continue
}
try {
verifiedCreationTransactions.push(v.parse(creationTransactionDbSchema, item))
} catch (e) {
throw new DatabaseError('load contributions with contribution link id', item, e as Error)
}
}
return verifiedCreationTransactions
}
}

View File

@ -0,0 +1,132 @@
import { and, asc, eq, isNull } from 'drizzle-orm'
import { alias } from 'drizzle-orm/mysql-core'
import {
AccountBalances,
AuthenticatedEncryption,
EncryptedMemo,
GradidoTransactionBuilder,
KeyPairEd25519,
MemoryBlockPtr,
TransferAmount
} from 'gradido-blockchain-js'
import * as v from 'valibot'
import { addToBlockchain } from '../../blockchain'
import { ContributionStatus } from '../../data/ContributionStatus'
import {
contributionsTable,
usersTable
} from '../../drizzle.schema'
import { BlockchainError, DatabaseError } from '../../errors'
import { CommunityContext, CreationTransactionDb, creationTransactionDbSchema } from '../../valibot.schema'
import { AbstractSyncRole } from './AbstractSync.role'
export class CreationsSyncRole extends AbstractSyncRole<CreationTransactionDb> {
getDate(): Date {
return this.peek().confirmedAt
}
itemTypeName(): string {
return 'creationTransactions'
}
async loadFromDb(offset: number, count: number): Promise<CreationTransactionDb[]> {
const confirmedByUsers = alias(usersTable, 'confirmedByUser')
const result = await this.context.db
.select({
contribution: contributionsTable,
user: usersTable,
confirmedByUser: confirmedByUsers,
})
.from(contributionsTable)
.where(and(
isNull(contributionsTable.contributionLinkId),
eq(contributionsTable.contributionStatus, ContributionStatus.CONFIRMED),
))
.innerJoin(usersTable, eq(contributionsTable.userId, usersTable.id))
.innerJoin(confirmedByUsers, eq(contributionsTable.confirmedBy, confirmedByUsers.id))
.orderBy(asc(contributionsTable.confirmedAt), asc(contributionsTable.transactionId))
.limit(count)
.offset(offset)
return result.map((row) => {
const item = {
...row.contribution,
user: row.user,
confirmedByUser: row.confirmedByUser,
}
try {
return v.parse(creationTransactionDbSchema, item)
} catch (e) {
throw new DatabaseError('loadCreations', item, e as Error)
}
})
}
buildTransaction(
item: CreationTransactionDb,
communityContext: CommunityContext,
recipientKeyPair: KeyPairEd25519,
signerKeyPair: KeyPairEd25519
): GradidoTransactionBuilder {
return new GradidoTransactionBuilder()
.setCreatedAt(item.confirmedAt)
.addMemo(
new EncryptedMemo(
item.memo,
new AuthenticatedEncryption(communityContext.keyPair),
new AuthenticatedEncryption(recipientKeyPair),
),
)
.setTransactionCreation(
new TransferAmount(recipientKeyPair.getPublicKey(), item.amount),
item.contributionDate,
)
.sign(signerKeyPair)
}
calculateAccountBalances(
item: CreationTransactionDb,
communityContext: CommunityContext,
recipientPublicKey: MemoryBlockPtr
): AccountBalances {
const accountBalances = new AccountBalances()
const balance = this.getLastBalanceForUser(recipientPublicKey, communityContext.blockchain)
// calculate decay since last balance with legacy calculation method
balance.updateLegacyDecay(item.amount, item.confirmedAt)
communityContext.aufBalance.updateLegacyDecay(item.amount, item.confirmedAt)
communityContext.gmwBalance.updateLegacyDecay(item.amount, item.confirmedAt)
accountBalances.add(balance.getAccountBalance())
accountBalances.add(communityContext.aufBalance.getAccountBalance())
accountBalances.add(communityContext.gmwBalance.getAccountBalance())
return accountBalances
}
pushToBlockchain(item: CreationTransactionDb): void {
const communityContext = this.context.getCommunityContextByUuid(item.user.communityUuid)
const blockchain = communityContext.blockchain
if (item.confirmedByUser.communityUuid !== item.user.communityUuid) {
throw new Error(`contribution was confirmed from other community: ${JSON.stringify(item, null, 2)}`)
}
const recipientKeyPair = this.getAccountKeyPair(communityContext, item.user.gradidoId)
const recipientPublicKey = recipientKeyPair.getPublicKey()
const signerKeyPair = this.getAccountKeyPair(communityContext, item.confirmedByUser.gradidoId)
if (!recipientKeyPair || !signerKeyPair || !recipientPublicKey) {
throw new Error(`missing key for ${this.itemTypeName()}: ${JSON.stringify(item, null, 2)}`)
}
try {
addToBlockchain(
this.buildTransaction(item, communityContext, recipientKeyPair, signerKeyPair),
blockchain,
item.id,
this.calculateAccountBalances(item, communityContext, recipientPublicKey),
)
} catch(e) {
throw new BlockchainError(`Error adding ${this.itemTypeName()}`, item, e as Error)
}
}
}

View File

@ -1,13 +1,148 @@
import { loadDeletedTransactionLinks } from '../../database'
import { TransactionDb } from '../../valibot.schema'
import { TransactionsSyncRole } from './TransactionsSync.role'
import { CommunityContext, DeletedTransactionLinkDb, deletedTransactionLinKDbSchema } from '../../valibot.schema'
import { AbstractSyncRole } from './AbstractSync.role'
import { transactionLinksTable, usersTable } from '../../drizzle.schema'
import { and, lt, asc, isNotNull, eq } from 'drizzle-orm'
import * as v from 'valibot'
import {
AccountBalance,
AccountBalances,
Filter,
GradidoDeferredTransfer,
GradidoTransactionBuilder,
GradidoTransfer,
GradidoUnit,
KeyPairEd25519,
MemoryBlockPtr,
TransferAmount
} from 'gradido-blockchain-js'
import { deriveFromCode } from '../../../../data/deriveKeyPair'
import { addToBlockchain } from '../../blockchain'
import { BlockchainError, DatabaseError } from '../../errors'
import { Balance } from '../../data/Balance'
export class DeletedTransactionLinksSyncRole extends AbstractSyncRole<DeletedTransactionLinkDb> {
getDate(): Date {
return this.peek().deletedAt
}
export class DeletedTransactionLinksSyncRole extends TransactionsSyncRole {
itemTypeName(): string {
return 'deletedTransactionLinks'
}
async loadFromDb(offset: number, count: number): Promise<TransactionDb[]> {
return await loadDeletedTransactionLinks(this.context.db, offset, count)
async loadFromDb(offset: number, count: number): Promise<DeletedTransactionLinkDb[]> {
const result = await this.context.db
.select({
transactionLink: transactionLinksTable,
user: usersTable,
})
.from(transactionLinksTable)
.where(
and(
isNotNull(transactionLinksTable.deletedAt),
lt(transactionLinksTable.deletedAt, transactionLinksTable.validUntil)
)
)
.innerJoin(usersTable, eq(transactionLinksTable.userId, usersTable.id))
.orderBy(asc(transactionLinksTable.deletedAt), asc(transactionLinksTable.id))
.limit(count)
.offset(offset)
return result.map((row) => {
const item = {
...row.transactionLink,
user: row.user,
}
try {
return v.parse(deletedTransactionLinKDbSchema, item)
} catch (e) {
throw new DatabaseError('loadDeletedTransactionLinks', item, e as Error)
}
})
}
buildTransaction(
item: DeletedTransactionLinkDb,
linkFundingTransactionNr: number,
restAmount: GradidoUnit,
senderKeyPair: KeyPairEd25519,
linkFundingPublicKey: MemoryBlockPtr,
): GradidoTransactionBuilder {
return new GradidoTransactionBuilder()
.setCreatedAt(item.deletedAt)
.setRedeemDeferredTransfer(
linkFundingTransactionNr,
new GradidoTransfer(
new TransferAmount(senderKeyPair.getPublicKey(), restAmount),
linkFundingPublicKey,
),
)
.sign(senderKeyPair)
}
calculateBalances(
item: DeletedTransactionLinkDb,
fundingTransaction: GradidoDeferredTransfer,
senderLastBalance: Balance,
communityContext: CommunityContext,
senderPublicKey: MemoryBlockPtr,
): AccountBalances {
const accountBalances = new AccountBalances()
const fundingUserLastBalance = this.getLastBalanceForUser(fundingTransaction.getSenderPublicKey()!, communityContext.blockchain)
fundingUserLastBalance.updateLegacyDecay(senderLastBalance.getBalance(), item.deletedAt)
// account of link is set to zero, gdd will be send back to initiator
accountBalances.add(new AccountBalance(senderPublicKey, GradidoUnit.zero(), ''))
accountBalances.add(fundingUserLastBalance.getAccountBalance())
return accountBalances
}
pushToBlockchain(item: DeletedTransactionLinkDb): void {
const communityContext = this.context.getCommunityContextByUuid(item.user.communityUuid)
const blockchain = communityContext.blockchain
const senderKeyPair = deriveFromCode(item.code)
const senderPublicKey = senderKeyPair.getPublicKey()
if (!senderKeyPair || !senderPublicKey) {
throw new Error(`missing key for ${this.itemTypeName()}: ${JSON.stringify(item, null, 2)}`)
}
const transaction = blockchain.findOne(Filter.lastBalanceFor(senderPublicKey))
if (!transaction) {
throw new Error(`expect transaction for code: ${item.code}`)
}
// should be funding transaction
if (!transaction.isDeferredTransfer()) {
throw new Error(`expect funding transaction: ${transaction.getConfirmedTransaction()?.toJson(true)}`)
}
const body = transaction.getConfirmedTransaction()?.getGradidoTransaction()?.getTransactionBody();
const deferredTransfer = body?.getDeferredTransfer()
if (!deferredTransfer || !deferredTransfer.getRecipientPublicKey()?.equal(senderPublicKey)) {
throw new Error(`expect funding transaction to belong to code: ${item.code}: ${transaction.getConfirmedTransaction()?.toJson(true)}`)
}
const linkFundingPublicKey = deferredTransfer.getSenderPublicKey()
if (!linkFundingPublicKey) {
throw new Error(`missing sender public key of transaction link founder`)
}
const senderLastBalance = this.getLastBalanceForUser(senderPublicKey, communityContext.blockchain)
senderLastBalance.updateLegacyDecay(GradidoUnit.zero(), item.deletedAt)
try {
addToBlockchain(
this.buildTransaction(
item, transaction.getTransactionNr(),
senderLastBalance.getBalance(),
senderKeyPair,
linkFundingPublicKey,
),
blockchain,
item.id,
this.calculateBalances(item, deferredTransfer, senderLastBalance, communityContext, senderPublicKey),
)
} catch(e) {
throw new BlockchainError(`Error adding ${this.itemTypeName()}`, item, e as Error)
}
}
}

View File

@ -0,0 +1,151 @@
import { and, asc, eq, isNotNull, isNull } from 'drizzle-orm'
import { alias } from 'drizzle-orm/mysql-core'
import {
AccountBalances,
AuthenticatedEncryption,
EncryptedMemo,
Filter,
GradidoTransactionBuilder,
KeyPairEd25519,
MemoryBlockPtr,
SearchDirection_DESC,
TransferAmount
} from 'gradido-blockchain-js'
import * as v from 'valibot'
import { addToBlockchain } from '../../blockchain'
import { TransactionTypeId } from '../../data/TransactionTypeId'
import { transactionsTable, usersTable } from '../../drizzle.schema'
import { BlockchainError, DatabaseError } from '../../errors'
import { CommunityContext, TransactionDb, transactionDbSchema } from '../../valibot.schema'
import { AbstractSyncRole } from './AbstractSync.role'
export class LocalTransactionsSyncRole extends AbstractSyncRole<TransactionDb> {
getDate(): Date {
return this.peek().balanceDate
}
itemTypeName(): string {
return 'localTransactions'
}
async loadFromDb(offset: number, count: number): Promise<TransactionDb[]> {
const linkedUsers = alias(usersTable, 'linkedUser')
const result = await this.context.db
.select({
transaction: transactionsTable,
user: usersTable,
linkedUser: linkedUsers,
})
.from(transactionsTable)
.where(
and(
eq(transactionsTable.typeId, TransactionTypeId.RECEIVE),
isNull(transactionsTable.transactionLinkId),
isNotNull(transactionsTable.linkedUserId),
eq(usersTable.foreign, 0),
eq(linkedUsers.foreign, 0),
)
)
.innerJoin(usersTable, eq(transactionsTable.userId, usersTable.id))
.innerJoin(linkedUsers, eq(transactionsTable.linkedUserId, linkedUsers.id))
.orderBy(asc(transactionsTable.balanceDate), asc(transactionsTable.id))
.limit(count)
.offset(offset)
return result.map((row) => {
const item = {
...row.transaction,
user: row.user,
linkedUser: row.linkedUser,
}
try {
return v.parse(transactionDbSchema, item)
} catch (e) {
throw new DatabaseError('loadLocalTransferTransactions', item, e as Error)
}
})
}
buildTransaction(
item: TransactionDb,
senderKeyPair: KeyPairEd25519,
recipientKeyPair: KeyPairEd25519,
): GradidoTransactionBuilder {
return new GradidoTransactionBuilder()
.setCreatedAt(item.balanceDate)
.addMemo(
new EncryptedMemo(
item.memo,
new AuthenticatedEncryption(senderKeyPair),
new AuthenticatedEncryption(recipientKeyPair),
),
)
.setTransactionTransfer(
new TransferAmount(senderKeyPair.getPublicKey(), item.amount),
recipientKeyPair.getPublicKey(),
)
.sign(senderKeyPair)
}
calculateBalances(
item: TransactionDb,
communityContext: CommunityContext,
senderPublicKey: MemoryBlockPtr,
recipientPublicKey: MemoryBlockPtr,
): AccountBalances {
const accountBalances = new AccountBalances()
const senderLastBalance = this.getLastBalanceForUser(senderPublicKey, communityContext.blockchain)
const recipientLastBalance = this.getLastBalanceForUser(recipientPublicKey, communityContext.blockchain)
if (senderLastBalance.getAccountBalance().getBalance().lt(item.amount)) {
const f = new Filter()
f.updatedBalancePublicKey = senderPublicKey
f.searchDirection = SearchDirection_DESC
f.pagination.size = 5
const lastTransactions = communityContext.blockchain.findAll(f)
for (let i = lastTransactions.size() - 1; i >= 0; i--) {
const tx = lastTransactions.get(i)
this.context.logger.error(`${tx?.getConfirmedTransaction()!.toJson(true)}`)
}
throw new Error(`sender has not enough balance (${senderLastBalance.getAccountBalance().getBalance().toString()}) to send ${item.amount.toString()} to ${recipientPublicKey.convertToHex()}`)
}
senderLastBalance.updateLegacyDecay(item.amount.negated(), item.balanceDate)
recipientLastBalance.updateLegacyDecay(item.amount, item.balanceDate)
accountBalances.add(senderLastBalance.getAccountBalance())
accountBalances.add(recipientLastBalance.getAccountBalance())
return accountBalances
}
pushToBlockchain(item: TransactionDb): void {
const communityContext = this.context.getCommunityContextByUuid(item.user.communityUuid)
const blockchain = communityContext.blockchain
if (item.linkedUser.communityUuid !== item.user.communityUuid) {
throw new Error(`transfer between user from different communities: ${JSON.stringify(item, null, 2)}`)
}
// I use the received transaction so user and linked user are swapped and user is recipient and linkedUser ist sender
const senderKeyPair = this.getAccountKeyPair(communityContext, item.linkedUser.gradidoId)
const senderPublicKey = senderKeyPair.getPublicKey()
const recipientKeyPair = this.getAccountKeyPair(communityContext, item.user.gradidoId)
const recipientPublicKey = recipientKeyPair.getPublicKey()
if (!senderKeyPair || !senderPublicKey || !recipientKeyPair || !recipientPublicKey) {
throw new Error(`missing key for ${this.itemTypeName()}: ${JSON.stringify(item, null, 2)}`)
}
try {
addToBlockchain(
this.buildTransaction(item, senderKeyPair, recipientKeyPair),
blockchain,
item.id,
this.calculateBalances(item, communityContext, senderPublicKey, recipientPublicKey),
)
} catch(e) {
throw new BlockchainError(`Error adding ${this.itemTypeName()}`, item, e as Error)
}
}
}

View File

@ -0,0 +1,161 @@
import { and, asc, eq, isNotNull, isNull } from 'drizzle-orm'
import {
AccountBalance,
AccountBalances,
AuthenticatedEncryption,
EncryptedMemo,
Filter,
GradidoDeferredTransfer,
GradidoTransactionBuilder,
GradidoTransfer,
GradidoUnit,
KeyPairEd25519,
MemoryBlockPtr,
TransferAmount
} from 'gradido-blockchain-js'
import * as v from 'valibot'
import { addToBlockchain } from '../../blockchain'
import { transactionLinksTable, usersTable } from '../../drizzle.schema'
import { BlockchainError, DatabaseError } from '../../errors'
import { CommunityContext, RedeemedTransactionLinkDb, redeemedTransactionLinkDbSchema } from '../../valibot.schema'
import { AbstractSyncRole } from './AbstractSync.role'
import { deriveFromCode } from '../../../../data/deriveKeyPair'
import { alias } from 'drizzle-orm/mysql-core'
export class RedeemTransactionLinksSyncRole extends AbstractSyncRole<RedeemedTransactionLinkDb> {
getDate(): Date {
return this.peek().redeemedAt
}
itemTypeName(): string {
return 'redeemTransactionLinks'
}
async loadFromDb(offset: number, count: number): Promise<RedeemedTransactionLinkDb[]> {
const redeemedByUser = alias(usersTable, 'redeemedByUser')
const result = await this.context.db
.select({
transactionLink: transactionLinksTable,
user: usersTable,
redeemedBy: redeemedByUser,
})
.from(transactionLinksTable)
.where(
and(
isNull(transactionLinksTable.deletedAt),
isNotNull(transactionLinksTable.redeemedAt),
eq(usersTable.foreign, 0),
eq(redeemedByUser.foreign, 0)
)
)
.innerJoin(usersTable, eq(transactionLinksTable.userId, usersTable.id))
.innerJoin(redeemedByUser, eq(transactionLinksTable.redeemedBy, redeemedByUser.id))
.orderBy(asc(transactionLinksTable.redeemedAt), asc(transactionLinksTable.id))
.limit(count)
.offset(offset)
return result.map((row) => {
const item = {
...row.transactionLink,
redeemedBy: row.redeemedBy,
user: row.user,
}
try {
return v.parse(redeemedTransactionLinkDbSchema, item)
} catch (e) {
throw new DatabaseError('loadRedeemTransactionLinks', item, e as Error)
}
})
}
buildTransaction(
item: RedeemedTransactionLinkDb,
linkFundingTransactionNr: number,
senderKeyPair: KeyPairEd25519,
recipientKeyPair: KeyPairEd25519,
): GradidoTransactionBuilder {
return new GradidoTransactionBuilder()
.setCreatedAt(item.redeemedAt)
.addMemo(
new EncryptedMemo(
item.memo,
new AuthenticatedEncryption(senderKeyPair),
new AuthenticatedEncryption(recipientKeyPair),
),
)
.setRedeemDeferredTransfer(
linkFundingTransactionNr,
new GradidoTransfer(
new TransferAmount(senderKeyPair.getPublicKey(), item.amount),
recipientKeyPair.getPublicKey(),
),
)
.sign(senderKeyPair)
}
calculateBalances(
item: RedeemedTransactionLinkDb,
fundingTransaction: GradidoDeferredTransfer,
communityContext: CommunityContext,
senderPublicKey: MemoryBlockPtr,
recipientPublicKey: MemoryBlockPtr,
): AccountBalances {
const accountBalances = new AccountBalances()
const senderLastBalance = this.getLastBalanceForUser(senderPublicKey, communityContext.blockchain)
const fundingUserLastBalance = this.getLastBalanceForUser(fundingTransaction.getSenderPublicKey()!, communityContext.blockchain)
const recipientLastBalance = this.getLastBalanceForUser(recipientPublicKey, communityContext.blockchain)
if (senderLastBalance.getAccountBalance().getBalance().lt(item.amount)) {
throw new Error(`sender has not enough balance (${senderLastBalance.getAccountBalance().getBalance().toString()}) to send ${item.amount.toString()} to ${recipientPublicKey.convertToHex()}`)
}
senderLastBalance.updateLegacyDecay(item.amount.negated(), item.redeemedAt)
fundingUserLastBalance.updateLegacyDecay(senderLastBalance.getBalance(), item.redeemedAt)
recipientLastBalance.updateLegacyDecay(item.amount, item.redeemedAt)
// account of link is set to zero, and change send back to link creator
accountBalances.add(new AccountBalance(senderPublicKey, GradidoUnit.zero(), ''))
accountBalances.add(recipientLastBalance.getAccountBalance())
accountBalances.add(fundingUserLastBalance.getAccountBalance())
return accountBalances
}
pushToBlockchain(item: RedeemedTransactionLinkDb): void {
const communityContext = this.context.getCommunityContextByUuid(item.user.communityUuid)
const blockchain = communityContext.blockchain
const senderKeyPair = deriveFromCode(item.code)
const senderPublicKey = senderKeyPair.getPublicKey()
const recipientKeyPair = this.getAccountKeyPair(communityContext, item.redeemedBy.gradidoId)
const recipientPublicKey = recipientKeyPair.getPublicKey()
if (!senderKeyPair || !senderPublicKey || !recipientKeyPair || !recipientPublicKey) {
throw new Error(`missing key for ${this.itemTypeName()}: ${JSON.stringify(item, null, 2)}`)
}
const transaction = blockchain.findOne(Filter.lastBalanceFor(senderPublicKey))
if (!transaction) {
throw new Error(`expect transaction for code: ${item.code}`)
}
// should be funding transaction
if (!transaction.isDeferredTransfer()) {
throw new Error(`expect funding transaction: ${transaction.getConfirmedTransaction()?.toJson(true)}`)
}
const body = transaction.getConfirmedTransaction()?.getGradidoTransaction()?.getTransactionBody();
const deferredTransfer = body?.getDeferredTransfer()
if (!deferredTransfer || !deferredTransfer.getRecipientPublicKey()?.equal(senderPublicKey)) {
throw new Error(`expect funding transaction to belong to code: ${item.code}: ${transaction.getConfirmedTransaction()?.toJson(true)}`)
}
try {
addToBlockchain(
this.buildTransaction(item, transaction.getTransactionNr(), senderKeyPair, recipientKeyPair),
blockchain,
item.id,
this.calculateBalances(item, deferredTransfer, communityContext, senderPublicKey, recipientPublicKey),
)
} catch(e) {
throw new BlockchainError(`Error adding ${this.itemTypeName()}`, item, e as Error)
}
}
}

View File

@ -0,0 +1,138 @@
import { asc, eq } from 'drizzle-orm'
import {
AccountBalance,
AccountBalances,
AuthenticatedEncryption,
DurationSeconds,
EncryptedMemo,
Filter,
GradidoTransactionBuilder,
GradidoTransfer,
GradidoUnit,
KeyPairEd25519,
MemoryBlockPtr,
SearchDirection_DESC,
TransferAmount
} from 'gradido-blockchain-js'
import * as v from 'valibot'
import { addToBlockchain } from '../../blockchain'
import { transactionLinksTable, usersTable } from '../../drizzle.schema'
import { BlockchainError, DatabaseError } from '../../errors'
import { CommunityContext, TransactionLinkDb, transactionLinkDbSchema } from '../../valibot.schema'
import { AbstractSyncRole } from './AbstractSync.role'
import { deriveFromCode } from '../../../../data/deriveKeyPair'
export class TransactionLinkFundingsSyncRole extends AbstractSyncRole<TransactionLinkDb> {
getDate(): Date {
return this.peek().createdAt
}
itemTypeName(): string {
return 'transactionLinkFundings'
}
async loadFromDb(offset: number, count: number): Promise<TransactionLinkDb[]> {
const result = await this.context.db
.select()
.from(transactionLinksTable)
.innerJoin(usersTable, eq(transactionLinksTable.userId, usersTable.id))
.orderBy(asc(transactionLinksTable.createdAt), asc(transactionLinksTable.id))
.limit(count)
.offset(offset)
return result.map((row) => {
const item = {
...row.transaction_links,
user: row.users,
}
try {
return v.parse(transactionLinkDbSchema, item)
} catch (e) {
throw new DatabaseError('loadTransactionLinkFundings', item, e as Error)
}
})
}
buildTransaction(
item: TransactionLinkDb,
blockedAmount: GradidoUnit,
duration: DurationSeconds,
senderKeyPair: KeyPairEd25519,
recipientKeyPair: KeyPairEd25519,
): GradidoTransactionBuilder {
return new GradidoTransactionBuilder()
.setCreatedAt(item.createdAt)
.addMemo(
new EncryptedMemo(
item.memo,
new AuthenticatedEncryption(senderKeyPair),
new AuthenticatedEncryption(recipientKeyPair),
),
)
.setDeferredTransfer(
new GradidoTransfer(
new TransferAmount(senderKeyPair.getPublicKey(), blockedAmount),
recipientKeyPair.getPublicKey(),
),
duration,
)
.sign(senderKeyPair)
}
calculateBalances(
item: TransactionLinkDb,
blockedAmount: GradidoUnit,
communityContext: CommunityContext,
senderPublicKey: MemoryBlockPtr,
recipientPublicKey: MemoryBlockPtr,
): AccountBalances {
const accountBalances = new AccountBalances()
const senderLastBalance = this.getLastBalanceForUser(senderPublicKey, communityContext.blockchain)
if (senderLastBalance.getBalance().lt(blockedAmount)) {
const f = new Filter()
f.updatedBalancePublicKey = senderPublicKey
f.pagination.size = 4
f.searchDirection = SearchDirection_DESC
const lastSenderTransactions = communityContext.blockchain.findAll(f)
this.context.logger.error(`sender hasn't enough balance: ${senderPublicKey.convertToHex()}, last ${lastSenderTransactions.size()} balance changing transactions:`)
for(let i = lastSenderTransactions.size() - 1; i >= 0; i--) {
const lastSenderTransaction = lastSenderTransactions.get(i)
this.context.logger.error(`${lastSenderTransaction?.getConfirmedTransaction()?.toJson(true)}`)
}
}
senderLastBalance.updateLegacyDecay(blockedAmount.negated(), item.createdAt)
accountBalances.add(senderLastBalance.getAccountBalance())
accountBalances.add(new AccountBalance(recipientPublicKey, blockedAmount, ''))
return accountBalances
}
pushToBlockchain(item: TransactionLinkDb): void {
const communityContext = this.context.getCommunityContextByUuid(item.user.communityUuid)
const blockchain = communityContext.blockchain
const senderKeyPair = this.getAccountKeyPair(communityContext, item.user.gradidoId)
const senderPublicKey = senderKeyPair.getPublicKey()
const recipientKeyPair = deriveFromCode(item.code)
const recipientPublicKey = recipientKeyPair.getPublicKey()
if (!senderKeyPair || !senderPublicKey || !recipientKeyPair || !recipientPublicKey) {
throw new Error(`missing key for ${this.itemTypeName()}: ${JSON.stringify(item, null, 2)}`)
}
const duration = new DurationSeconds((item.validUntil.getTime() - item.createdAt.getTime()) / 1000)
const blockedAmount = item.amount.calculateCompoundInterest(duration.getSeconds())
try {
addToBlockchain(
this.buildTransaction(item, blockedAmount, duration, senderKeyPair, recipientKeyPair),
blockchain,
item.id,
this.calculateBalances(item, blockedAmount, communityContext, senderPublicKey, recipientPublicKey),
)
} catch(e) {
throw new BlockchainError(`Error adding ${this.itemTypeName()}`, item, e as Error)
}
}
}

View File

@ -1,45 +0,0 @@
import { AccountBalance, AccountBalances, Filter, MemoryBlockPtr, SearchDirection_DESC } from 'gradido-blockchain-js'
import { KeyPairIdentifierLogic } from '../../../../data/KeyPairIdentifier.logic'
import { ResolveKeyPair } from '../../../../interactions/resolveKeyPair/ResolveKeyPair.context'
import { addTransaction } from '../../blockchain'
import { transactionLinkDbToTransaction } from '../../convert'
import { loadTransactionLinks } from '../../database'
import { TransactionLinkDb } from '../../valibot.schema'
import { AbstractSyncRole } from './AbstractSync.role'
export class TransactionLinksSyncRole extends AbstractSyncRole<TransactionLinkDb> {
getDate(): Date {
return this.peek().createdAt
}
itemTypeName(): string {
return 'transactionLinks'
}
async loadFromDb(offset: number, count: number): Promise<TransactionLinkDb[]> {
return await loadTransactionLinks(this.context.db, offset, count)
}
async pushToBlockchain(item: TransactionLinkDb): Promise<void> {
const communityContext = this.context.getCommunityContextByUuid(item.user.communityUuid)
const transaction = transactionLinkDbToTransaction(item, communityContext.topicId)
// I use the receiving part of transaction pair, so the user is the recipient and the linked user the sender
const accountBalances = new AccountBalances()
const senderKeyPair = await ResolveKeyPair(
new KeyPairIdentifierLogic(transaction.user),
)
const recipientKeyPair = await ResolveKeyPair(
new KeyPairIdentifierLogic(transaction.linkedUser!),
)
const f = new Filter()
f.involvedPublicKey = senderKeyPair.getPublicKey()
f.pagination.size = 1
f.searchDirection = SearchDirection_DESC
communityContext.blockchain.findOne(f)
accountBalances.add(new AccountBalance(senderKeyPair.getPublicKey(), item.linkedUserBalance, ''))
accountBalances.add(new AccountBalance(recipientKeyPair.getPublicKey(), item.amount, ''))
await addTransaction(communityContext.blockchain, communityContext.blockchain, transaction, item.id, accountBalances)
}
}

View File

@ -1,116 +0,0 @@
import Decimal from 'decimal.js-light'
import { AccountBalance, AccountBalances, GradidoUnit } from 'gradido-blockchain-js'
import { InputTransactionType } from '../../../../data/InputTransactionType.enum'
import { KeyPairIdentifierLogic } from '../../../../data/KeyPairIdentifier.logic'
import { GradidoBlockchainCryptoError } from '../../../../errors'
import { ResolveKeyPair } from '../../../../interactions/resolveKeyPair/ResolveKeyPair.context'
import { AUF_ACCOUNT_DERIVATION_INDEX, GMW_ACCOUNT_DERIVATION_INDEX, hardenDerivationIndex } from '../../../../utils/derivationHelper'
import { addTransaction } from '../../blockchain'
import { transactionDbToTransaction } from '../../convert'
import { loadTransactions } from '../../database'
import { legacyCalculateDecay } from '../../utils'
import { TransactionDb } from '../../valibot.schema'
import { AbstractSyncRole } from './AbstractSync.role'
type BalanceDate = {
balance: Decimal
date: Date
}
export class TransactionsSyncRole extends AbstractSyncRole<TransactionDb> {
private static transactionLinkCodes = new Set<string>()
static doubleTransactionLinkCodes: string[] = []
getDate(): Date {
return this.peek().balanceDate
}
itemTypeName(): string {
return 'transactions'
}
async loadFromDb(offset: number, count: number): Promise<TransactionDb[]> {
const result = await loadTransactions(this.context.db, offset, count)
return result.filter((item) => {
if (item.transactionLinkCode) {
if (TransactionsSyncRole.transactionLinkCodes.has(item.transactionLinkCode)) {
TransactionsSyncRole.doubleTransactionLinkCodes.push(item.transactionLinkCode)
return false
}
TransactionsSyncRole.transactionLinkCodes.add(item.transactionLinkCode)
}
return true
})
}
async pushToBlockchain(item: TransactionDb): Promise<void> {
const senderCommunityContext = this.context.getCommunityContextByUuid(item.user.communityUuid)
const recipientCommunityContext = this.context.getCommunityContextByUuid(
item.linkedUser.communityUuid,
)
this.context.cache.setHomeCommunityTopicId(senderCommunityContext.topicId)
const transaction = transactionDbToTransaction(
item,
senderCommunityContext.topicId,
recipientCommunityContext.topicId,
)
const accountBalances = new AccountBalances()
if (InputTransactionType.GRADIDO_CREATION === transaction.type) {
const recipientKeyPair = await ResolveKeyPair(
new KeyPairIdentifierLogic(transaction.linkedUser!),
)
accountBalances.add(new AccountBalance(recipientKeyPair.getPublicKey(), item.balance, ''))
// update gmw and auf
this.updateGmwAuf(new Decimal(item.amount.toString(4)), item.balanceDate)
const communityKeyPair = await ResolveKeyPair(new KeyPairIdentifierLogic({ communityTopicId: senderCommunityContext.topicId }))
const gmwKeyPair = communityKeyPair.deriveChild(
hardenDerivationIndex(GMW_ACCOUNT_DERIVATION_INDEX),
)
if (!gmwKeyPair) {
throw new GradidoBlockchainCryptoError(
`KeyPairEd25519 child derivation failed, has private key: ${communityKeyPair.hasPrivateKey()} for community: ${senderCommunityContext.communityId}`,
)
}
const aufKeyPair = communityKeyPair.deriveChild(
hardenDerivationIndex(AUF_ACCOUNT_DERIVATION_INDEX),
)
if (!aufKeyPair) {
throw new GradidoBlockchainCryptoError(
`KeyPairEd25519 child derivation failed, has private key: ${communityKeyPair.hasPrivateKey()} for community: ${senderCommunityContext.communityId}`,
)
}
accountBalances.add(new AccountBalance(gmwKeyPair.getPublicKey(), GradidoUnit.fromString(
TransactionsSyncRole.gmwBalance!.balance.toString()), ''))
accountBalances.add(new AccountBalance(aufKeyPair.getPublicKey(), GradidoUnit.fromString(
TransactionsSyncRole.aufBalance!.balance.toString()), ''))
} else if (InputTransactionType.REGISTER_ADDRESS === transaction.type) {
const recipientKeyPair = await ResolveKeyPair(
new KeyPairIdentifierLogic(transaction.user),
)
accountBalances.add(new AccountBalance(recipientKeyPair.getPublicKey(), GradidoUnit.zero(), ''))
} else {
// I use the receiving part of transaction pair, so the user is the recipient and the linked user the sender
const senderKeyPair = await ResolveKeyPair(
new KeyPairIdentifierLogic(transaction.linkedUser!),
)
const recipientKeyPair = await ResolveKeyPair(
new KeyPairIdentifierLogic(transaction.user),
)
accountBalances.add(new AccountBalance(senderKeyPair.getPublicKey(), item.linkedUserBalance, ''))
accountBalances.add(new AccountBalance(recipientKeyPair.getPublicKey(), item.balance, ''))
}
try {
await addTransaction(
senderCommunityContext.blockchain,
recipientCommunityContext.blockchain,
transaction,
item.id,
accountBalances,
)
} catch(e) {
this.context.logger.error(`error adding transaction: ${JSON.stringify(transaction, null, 2)}`)
throw e
}
}
}

View File

@ -1,11 +1,24 @@
import { addTransaction } from '../../blockchain'
import { userDbToTransaction } from '../../convert'
import { loadUsers } from '../../database'
import { generateKeyPairUserAccount } from '../../data/keyPair'
import { CreatedUserDb } from '../../valibot.schema'
import { asc } from 'drizzle-orm'
import {
AccountBalance,
AccountBalances,
AddressType_COMMUNITY_HUMAN,
GradidoTransactionBuilder,
GradidoUnit,
KeyPairEd25519,
MemoryBlockPtr
} from 'gradido-blockchain-js'
import * as v from 'valibot'
import { deriveFromKeyPairAndUuid } from '../../../../data/deriveKeyPair'
import { Uuidv4Hash } from '../../../../data/Uuidv4Hash'
import { addToBlockchain } from '../../blockchain'
import { usersTable } from '../../drizzle.schema'
import { BlockchainError, DatabaseError } from '../../errors'
import { UserDb, userDbSchema } from '../../valibot.schema'
import { AbstractSyncRole } from './AbstractSync.role'
export class UsersSyncRole extends AbstractSyncRole<CreatedUserDb> {
export class UsersSyncRole extends AbstractSyncRole<UserDb> {
getDate(): Date {
return this.peek().createdAt
}
@ -14,18 +27,66 @@ export class UsersSyncRole extends AbstractSyncRole<CreatedUserDb> {
return 'users'
}
async loadFromDb(offset: number, count: number): Promise<CreatedUserDb[]> {
const users = await loadUsers(this.context.db, offset, count)
for (const user of users) {
const communityContext = this.context.getCommunityContextByUuid(user.communityUuid)
await generateKeyPairUserAccount(user, this.context.cache, communityContext.topicId)
}
return users
async loadFromDb(offset: number, count: number): Promise<UserDb[]> {
const result = await this.context.db
.select()
.from(usersTable)
.orderBy(asc(usersTable.createdAt), asc(usersTable.id))
.limit(count)
.offset(offset)
return result.map((row) => {
try {
return v.parse(userDbSchema, row)
} catch (e) {
throw new DatabaseError('loadUsers', row, e as Error)
}
})
}
async pushToBlockchain(item: CreatedUserDb): Promise<void> {
buildTransaction(
item: UserDb,
communityKeyPair: KeyPairEd25519,
accountKeyPair: KeyPairEd25519,
userKeyPair: KeyPairEd25519
): GradidoTransactionBuilder {
return new GradidoTransactionBuilder()
.setCreatedAt(item.createdAt)
.setRegisterAddress(
userKeyPair.getPublicKey(),
AddressType_COMMUNITY_HUMAN,
new Uuidv4Hash(item.gradidoId).getAsMemoryBlock(),
accountKeyPair.getPublicKey(),
)
.sign(communityKeyPair)
.sign(accountKeyPair)
.sign(userKeyPair)
}
calculateAccountBalances(accountPublicKey: MemoryBlockPtr): AccountBalances {
const accountBalances = new AccountBalances()
accountBalances.add(new AccountBalance(accountPublicKey, GradidoUnit.zero(), ''))
return accountBalances
}
pushToBlockchain(item: UserDb): void {
const communityContext = this.context.getCommunityContextByUuid(item.communityUuid)
const transaction = userDbToTransaction(item, communityContext.topicId)
return await addTransaction(communityContext.blockchain, communityContext.blockchain, transaction, item.id)
const userKeyPair = deriveFromKeyPairAndUuid(communityContext.keyPair, item.gradidoId)
const accountKeyPair = this.getAccountKeyPair(communityContext, item.gradidoId)
const accountPublicKey = accountKeyPair.getPublicKey()
if (!userKeyPair || !accountKeyPair || !accountPublicKey) {
throw new Error(`missing key for ${this.itemTypeName()}: ${JSON.stringify(item, null, 2)}`)
}
try {
addToBlockchain(
this.buildTransaction(item, communityContext.keyPair, accountKeyPair, userKeyPair),
communityContext.blockchain,
item.id,
this.calculateAccountBalances(accountPublicKey),
)
} catch (e) {
throw new BlockchainError(`Error adding ${this.itemTypeName()}`, item, e as Error)
}
}
}

View File

@ -1,11 +1,13 @@
import { Profiler } from 'gradido-blockchain-js'
import { Context } from '../../Context'
import { CreationsSyncRole } from './CreationsSync.role'
import { InvalidContributionTransactionSyncRole } from './InvalidContributionTransactionSync.role'
import { LocalTransactionsSyncRole } from './LocalTransactionsSync.role'
import { UsersSyncRole } from './UsersSync.role'
import { TransactionLinkFundingsSyncRole } from './TransactionLinkFundingsSync.role'
import { RedeemTransactionLinksSyncRole } from './RedeemTransactionLinksSync.role'
import { ContributionLinkTransactionSyncRole } from './ContributionLinkTransactionSync.role'
import { DeletedTransactionLinksSyncRole } from './DeletedTransactionLinksSync.role'
import { InvalidContributionTransactionSyncRole } from './InvalidContributionTransactionSync.role'
import { TransactionLinksSyncRole } from './TransactionLinksSync.role'
import { TransactionsSyncRole } from './TransactionsSync.role'
import { UsersSyncRole } from './UsersSync.role'
export async function syncDbWithBlockchainContext(context: Context, batchSize: number) {
const timeUsedDB = new Profiler()
@ -13,11 +15,12 @@ export async function syncDbWithBlockchainContext(context: Context, batchSize: n
const timeUsedAll = new Profiler()
const containers = [
new UsersSyncRole(context),
new TransactionsSyncRole(context),
new DeletedTransactionLinksSyncRole(context),
new TransactionLinksSyncRole(context),
new InvalidContributionTransactionSyncRole(context),
new CreationsSyncRole(context),
new LocalTransactionsSyncRole(context),
new TransactionLinkFundingsSyncRole(context),
new RedeemTransactionLinksSyncRole(context),
new ContributionLinkTransactionSyncRole(context),
new DeletedTransactionLinksSyncRole(context),
]
let transactionsCount = 0
let transactionsCountSinceLastLog = 0
@ -38,10 +41,12 @@ export async function syncDbWithBlockchainContext(context: Context, batchSize: n
}
// sort by date, to ensure container on index 0 is the one with the smallest date
if (available.length > 0) {
if (available.length > 1) {
// const sortTime = new Profiler()
available.sort((a, b) => a.getDate().getTime() - b.getDate().getTime())
// context.logger.debug(`sorted ${available.length} containers in ${sortTime.string()}`)
}
await available[0].toBlockchain()
available[0].toBlockchain()
process.stdout.write(`successfully added to blockchain: ${transactionsCount}\r`)
transactionsCount++
transactionsCountSinceLastLog++
@ -57,8 +62,8 @@ export async function syncDbWithBlockchainContext(context: Context, batchSize: n
if (context.logger.isDebugEnabled()) {
context.logger.debug(InvalidContributionTransactionSyncRole.allTransactionIds.join(', '))
}
context.logger.info(`Double linked transactions: ${TransactionsSyncRole.doubleTransactionLinkCodes.length}`)
/*context.logger.info(`Double linked transactions: ${TransactionsSyncRole.doubleTransactionLinkCodes.length}`)
if (context.logger.isDebugEnabled()) {
context.logger.debug(TransactionsSyncRole.doubleTransactionLinkCodes.join(', '))
}
}*/
}

View File

@ -1,40 +1,36 @@
import { InMemoryBlockchain } from 'gradido-blockchain-js'
import { InMemoryBlockchain, KeyPairEd25519 } from 'gradido-blockchain-js'
import * as v from 'valibot'
import { booleanSchema, dateSchema } from '../../schemas/typeConverter.schema'
import {
gradidoAmountSchema,
hieroIdSchema,
identifierSeedSchema,
memoSchema,
uuidv4Schema,
} from '../../schemas/typeGuard.schema'
import { TransactionTypeId } from './data/TransactionTypeId'
import { Balance } from './data/Balance'
import { TransactionTypeId } from './data/TransactionTypeId'
export const createdUserDbSchema = v.object({
id: v.pipe(v.number(), v.minValue(1)),
const positiveNumberSchema = v.pipe(v.number(), v.minValue(1))
export const userDbSchema = v.object({
id: positiveNumberSchema,
gradidoId: uuidv4Schema,
communityUuid: uuidv4Schema,
createdAt: dateSchema,
})
export const userDbSchema = v.object({
gradidoId: uuidv4Schema,
communityUuid: uuidv4Schema,
export const transactionBaseSchema = v.object({
id: positiveNumberSchema,
amount: gradidoAmountSchema,
memo: memoSchema,
user: userDbSchema,
})
export const transactionDbSchema = v.pipe(v.object({
id: v.pipe(v.number(), v.minValue(1)),
...transactionBaseSchema.entries,
typeId: v.enum(TransactionTypeId),
amount: gradidoAmountSchema,
balanceDate: dateSchema,
balance: gradidoAmountSchema,
linkedUserBalance: gradidoAmountSchema,
memo: memoSchema,
creationDate: v.nullish(dateSchema),
user: createdUserDbSchema,
linkedUser: createdUserDbSchema,
transactionLinkCode: v.nullish(identifierSeedSchema),
linkedUser: userDbSchema,
}), v.custom((value: any) => {
if (value.user && value.linkedUser && !value.transactionLinkCode && value.user.gradidoId === value.linkedUser.gradidoId) {
throw new Error(`expect user to be different from linkedUser: ${JSON.stringify(value, null, 2)}`)
@ -53,29 +49,62 @@ export const transactionDbSchema = v.pipe(v.object({
return value
}))
export const creationTransactionDbSchema = v.pipe(v.object({
...transactionBaseSchema.entries,
contributionDate: dateSchema,
confirmedAt: dateSchema,
confirmedByUser: userDbSchema,
}), v.custom((value: any) => {
if (value.user && value.confirmedByUser && value.user.gradidoId === value.confirmedByUser.gradidoId) {
throw new Error(`expect user to be different from confirmedByUser: ${JSON.stringify(value, null, 2)}`)
}
// check that user and confirmedByUser exist before transaction balance date
const confirmedAt = new Date(value.confirmedAt)
if (
value.user.createdAt.getTime() >= confirmedAt.getTime() ||
value.confirmedByUser?.createdAt.getTime() >= confirmedAt.getTime()
) {
throw new Error(
`at least one user was created after transaction confirmedAt date, logic error! ${JSON.stringify(value, null, 2)}`,
)
}
return value
}))
export const transactionLinkDbSchema = v.object({
id: v.pipe(v.number(), v.minValue(1)),
user: userDbSchema,
...transactionBaseSchema.entries,
code: identifierSeedSchema,
amount: gradidoAmountSchema,
memo: memoSchema,
createdAt: dateSchema,
validUntil: dateSchema,
})
export const redeemedTransactionLinkDbSchema = v.object({
...transactionLinkDbSchema.entries,
redeemedAt: dateSchema,
redeemedBy: userDbSchema,
})
export const deletedTransactionLinKDbSchema = v.object({
id: positiveNumberSchema,
user: userDbSchema,
code: identifierSeedSchema,
deletedAt: dateSchema,
})
export const communityDbSchema = v.object({
id: positiveNumberSchema,
foreign: booleanSchema,
communityUuid: uuidv4Schema,
name: v.string(),
creationDate: dateSchema,
userMinCreatedAt: v.nullish(dateSchema),
uniqueAlias: v.string(),
})
export const communityContextSchema = v.object({
communityId: v.string(),
blockchain: v.instance(InMemoryBlockchain, 'expect InMemoryBlockchain type'),
topicId: hieroIdSchema,
keyPair: v.instance(KeyPairEd25519),
folder: v.pipe(
v.string(),
v.minLength(1, 'expect string length >= 1'),
@ -87,9 +116,10 @@ export const communityContextSchema = v.object({
})
export type TransactionDb = v.InferOutput<typeof transactionDbSchema>
export type CreationTransactionDb = v.InferOutput<typeof creationTransactionDbSchema>
export type UserDb = v.InferOutput<typeof userDbSchema>
export type CreatedUserDb = v.InferOutput<typeof createdUserDbSchema>
export type TransactionLinkDb = v.InferOutput<typeof transactionLinkDbSchema>
export type RedeemedTransactionLinkDb = v.InferOutput<typeof redeemedTransactionLinkDbSchema>
export type DeletedTransactionLinkDb = v.InferOutput<typeof deletedTransactionLinKDbSchema>
export type CommunityDb = v.InferOutput<typeof communityDbSchema>
export type Balance = v.InferOutput<typeof balanceSchema>
export type CommunityContext = v.InferOutput<typeof communityContextSchema>