diff --git a/database/migration/migrations/0097-fix_production_data_for_blockchain2.ts b/database/migration/migrations/0097-fix_production_data_for_blockchain2.ts index 004f5ccd3..14ed3f826 100644 --- a/database/migration/migrations/0097-fix_production_data_for_blockchain2.ts +++ b/database/migration/migrations/0097-fix_production_data_for_blockchain2.ts @@ -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. * diff --git a/dlt-connector/src/cache/KeyPairCacheManager.ts b/dlt-connector/src/cache/KeyPairCacheManager.ts index 8d7c3bf56..f38342904 100644 --- a/dlt-connector/src/cache/KeyPairCacheManager.ts +++ b/dlt-connector/src/cache/KeyPairCacheManager.ts @@ -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 + } } diff --git a/dlt-connector/src/data/deriveKeyPair.ts b/dlt-connector/src/data/deriveKeyPair.ts new file mode 100644 index 000000000..f0880b207 --- /dev/null +++ b/dlt-connector/src/data/deriveKeyPair.ts @@ -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 +} \ No newline at end of file diff --git a/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/Context.ts b/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/Context.ts index 40931ef1e..ea854ce7a 100644 --- a/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/Context.ts +++ b/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/Context.ts @@ -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 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) { diff --git a/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/blockchain.ts b/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/blockchain.ts index e8ad337bc..1d473a1b2 100644 --- a/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/blockchain.ts +++ b/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/blockchain.ts @@ -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( diff --git a/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/bootstrap.ts b/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/bootstrap.ts index 5e03fbff6..cd34bd8eb 100644 --- a/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/bootstrap.ts +++ b/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/bootstrap.ts @@ -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 { const context = await Context.create() @@ -26,62 +24,63 @@ export async function bootstrap(): Promise { async function bootstrapCommunities(context: Context): Promise> { const communities = new Map() const communitiesDb = await loadCommunities(context.db) - const topicIds = new Set() + const communityNames = new Set() 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 } diff --git a/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/data/Balance.ts b/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/data/Balance.ts index adc13860d..718be3ff9 100644 --- a/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/data/Balance.ts +++ b/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/data/Balance.ts @@ -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 || '') } } diff --git a/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/data/ContributionStatus.ts b/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/data/ContributionStatus.ts new file mode 100644 index 000000000..a4bbc55f3 --- /dev/null +++ b/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/data/ContributionStatus.ts @@ -0,0 +1,7 @@ +export enum ContributionStatus { + PENDING = 'PENDING', + DELETED = 'DELETED', + IN_PROGRESS = 'IN_PROGRESS', + DENIED = 'DENIED', + CONFIRMED = 'CONFIRMED', +} diff --git a/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/database.ts b/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/database.ts index 5000a22fc..a4a81b826 100644 --- a/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/database.ts +++ b/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/database.ts @@ -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() -export const adminUsers = new Map() +export const contributionLinkModerators = new Map() +export const adminUsers = new Map() const transactionIdSet = new Set() export async function loadContributionLinkModeratorCache(db: MySql2Database): Promise { @@ -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 { .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 { export async function loadCommunities(db: MySql2Database): Promise { 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() 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 { +): Promise { 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 { +export async function loadUserByGradidoId(db: MySql2Database, gradidoId: string): Promise { 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 { + 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 { 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 { + 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, diff --git a/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/drizzle.schema.ts b/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/drizzle.schema.ts index cb99275e9..3b7b2ec45 100644 --- a/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/drizzle.schema.ts +++ b/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/drizzle.schema.ts @@ -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`), }) diff --git a/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/errors.ts b/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/errors.ts index 85c5be50a..504ce76a4 100644 --- a/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/errors.ts +++ b/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/errors.ts @@ -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`) } } - \ No newline at end of file + +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 + } +} diff --git a/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/index.ts b/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/index.ts index 9803429e2..0d7f69805 100644 --- a/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/index.ts +++ b/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/index.ts @@ -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) diff --git a/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/interaction/accountBalances/AbstractBalances.role.ts b/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/interaction/accountBalances/AbstractBalances.role.ts index f2b2cbaf8..b2433240e 100644 --- a/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/interaction/accountBalances/AbstractBalances.role.ts +++ b/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/interaction/accountBalances/AbstractBalances.role.ts @@ -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 + + async getLastBalanceForUser(identifierAccount: IdentifierAccount, blockchain: InMemoryBlockchain): Promise { + 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()) + } } \ No newline at end of file diff --git a/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/interaction/accountBalances/CreationBalances.role.ts b/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/interaction/accountBalances/CreationBalances.role.ts index a596832d6..45d8e2308 100644 --- a/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/interaction/accountBalances/CreationBalances.role.ts +++ b/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/interaction/accountBalances/CreationBalances.role.ts @@ -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 { - -} \ No newline at end of file + + constructor(transaction: Transaction, protected dbTransaction: TransactionDb) { + super(transaction) + } + + async getAccountBalances(context: Context): Promise { + 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 + } +} diff --git a/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/interaction/accountBalances/DeferredTransferBalances.role.ts b/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/interaction/accountBalances/DeferredTransferBalances.role.ts new file mode 100644 index 000000000..077c20ca7 --- /dev/null +++ b/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/interaction/accountBalances/DeferredTransferBalances.role.ts @@ -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 { + 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 + } +} \ No newline at end of file diff --git a/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/interaction/accountBalances/RedeemDeferredTransferBalances.role.ts b/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/interaction/accountBalances/RedeemDeferredTransferBalances.role.ts new file mode 100644 index 000000000..4024c3dd3 --- /dev/null +++ b/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/interaction/accountBalances/RedeemDeferredTransferBalances.role.ts @@ -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 { + // 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 + } +} \ No newline at end of file diff --git a/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/interaction/accountBalances/RegisterAddressBalances.role.ts b/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/interaction/accountBalances/RegisterAddressBalances.role.ts new file mode 100644 index 000000000..0c4c53454 --- /dev/null +++ b/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/interaction/accountBalances/RegisterAddressBalances.role.ts @@ -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 { + const accountBalances = new AccountBalances() + const recipientKeyPair = await ResolveKeyPair( + new KeyPairIdentifierLogic(this.transaction.user), + ) + accountBalances.add(new AccountBalance(recipientKeyPair.getPublicKey(), GradidoUnit.zero(), '')) + return accountBalances + } +} \ No newline at end of file diff --git a/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/interaction/accountBalances/TransferBalances.role.ts b/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/interaction/accountBalances/TransferBalances.role.ts index e69de29bb..c5182627d 100644 --- a/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/interaction/accountBalances/TransferBalances.role.ts +++ b/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/interaction/accountBalances/TransferBalances.role.ts @@ -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 { + // 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 + } +} \ No newline at end of file diff --git a/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/interaction/accountBalances/accountBalances.context.ts b/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/interaction/accountBalances/accountBalances.context.ts index 348e5a8b6..6a2f63604 100644 --- a/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/interaction/accountBalances/accountBalances.context.ts +++ b/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/interaction/accountBalances/accountBalances.context.ts @@ -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 { + 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, '')) - */ \ No newline at end of file diff --git a/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/interaction/syncDbWithBlockchain/AbstractSync.role.ts b/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/interaction/syncDbWithBlockchain/AbstractSync.role.ts index 9038e0e49..df6ba0f19 100644 --- a/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/interaction/syncDbWithBlockchain/AbstractSync.role.ts +++ b/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/interaction/syncDbWithBlockchain/AbstractSync.role.ts @@ -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 { private items: T[] = [] @@ -14,9 +18,34 @@ export abstract class AbstractSyncRole { ) } + 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 - abstract pushToBlockchain(item: T): Promise + abstract pushToBlockchain(item: T): void abstract itemTypeName(): string // return count of new loaded items @@ -38,11 +67,11 @@ export abstract class AbstractSyncRole { return 0 } - async toBlockchain(): Promise { + 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 { diff --git a/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/interaction/syncDbWithBlockchain/ContributionLinkTransactionSync.role.ts b/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/interaction/syncDbWithBlockchain/ContributionLinkTransactionSync.role.ts index 27908fa19..41f488d4b 100644 --- a/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/interaction/syncDbWithBlockchain/ContributionLinkTransactionSync.role.ts +++ b/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/interaction/syncDbWithBlockchain/ContributionLinkTransactionSync.role.ts @@ -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 { - 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 { + 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 } } diff --git a/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/interaction/syncDbWithBlockchain/CreationsSync.role.ts b/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/interaction/syncDbWithBlockchain/CreationsSync.role.ts new file mode 100644 index 000000000..ff23e1fcb --- /dev/null +++ b/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/interaction/syncDbWithBlockchain/CreationsSync.role.ts @@ -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 { + + getDate(): Date { + return this.peek().confirmedAt + } + + itemTypeName(): string { + return 'creationTransactions' + } + + async loadFromDb(offset: number, count: number): Promise { + 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) + } + } +} diff --git a/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/interaction/syncDbWithBlockchain/DeletedTransactionLinksSync.role.ts b/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/interaction/syncDbWithBlockchain/DeletedTransactionLinksSync.role.ts index fd1ae225f..c1c12e724 100644 --- a/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/interaction/syncDbWithBlockchain/DeletedTransactionLinksSync.role.ts +++ b/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/interaction/syncDbWithBlockchain/DeletedTransactionLinksSync.role.ts @@ -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 { + getDate(): Date { + return this.peek().deletedAt + } -export class DeletedTransactionLinksSyncRole extends TransactionsSyncRole { itemTypeName(): string { return 'deletedTransactionLinks' } - async loadFromDb(offset: number, count: number): Promise { - return await loadDeletedTransactionLinks(this.context.db, offset, count) + async loadFromDb(offset: number, count: number): Promise { + 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) + } + } + } diff --git a/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/interaction/syncDbWithBlockchain/LocalTransactionsSync.role.ts b/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/interaction/syncDbWithBlockchain/LocalTransactionsSync.role.ts new file mode 100644 index 000000000..63b03ff36 --- /dev/null +++ b/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/interaction/syncDbWithBlockchain/LocalTransactionsSync.role.ts @@ -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 { + + getDate(): Date { + return this.peek().balanceDate + } + + itemTypeName(): string { + return 'localTransactions' + } + + async loadFromDb(offset: number, count: number): Promise { + 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) + } + } +} diff --git a/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/interaction/syncDbWithBlockchain/RedeemTransactionLinksSync.role.ts b/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/interaction/syncDbWithBlockchain/RedeemTransactionLinksSync.role.ts new file mode 100644 index 000000000..b2d0fd707 --- /dev/null +++ b/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/interaction/syncDbWithBlockchain/RedeemTransactionLinksSync.role.ts @@ -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 { + getDate(): Date { + return this.peek().redeemedAt + } + + itemTypeName(): string { + return 'redeemTransactionLinks' + } + + async loadFromDb(offset: number, count: number): Promise { + 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) + } + } +} diff --git a/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/interaction/syncDbWithBlockchain/TransactionLinkFundingsSync.role.ts b/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/interaction/syncDbWithBlockchain/TransactionLinkFundingsSync.role.ts new file mode 100644 index 000000000..a0dd98593 --- /dev/null +++ b/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/interaction/syncDbWithBlockchain/TransactionLinkFundingsSync.role.ts @@ -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 { + getDate(): Date { + return this.peek().createdAt + } + + itemTypeName(): string { + return 'transactionLinkFundings' + } + + async loadFromDb(offset: number, count: number): Promise { + 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) + } + } +} diff --git a/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/interaction/syncDbWithBlockchain/TransactionLinksSync.role.ts b/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/interaction/syncDbWithBlockchain/TransactionLinksSync.role.ts deleted file mode 100644 index 6920732cc..000000000 --- a/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/interaction/syncDbWithBlockchain/TransactionLinksSync.role.ts +++ /dev/null @@ -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 { - getDate(): Date { - return this.peek().createdAt - } - - itemTypeName(): string { - return 'transactionLinks' - } - - async loadFromDb(offset: number, count: number): Promise { - return await loadTransactionLinks(this.context.db, offset, count) - } - - async pushToBlockchain(item: TransactionLinkDb): Promise { - 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) - } -} diff --git a/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/interaction/syncDbWithBlockchain/TransactionsSync.role.ts b/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/interaction/syncDbWithBlockchain/TransactionsSync.role.ts deleted file mode 100644 index 2ebab70ef..000000000 --- a/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/interaction/syncDbWithBlockchain/TransactionsSync.role.ts +++ /dev/null @@ -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 { - private static transactionLinkCodes = new Set() - static doubleTransactionLinkCodes: string[] = [] - - getDate(): Date { - return this.peek().balanceDate - } - - itemTypeName(): string { - return 'transactions' - } - - async loadFromDb(offset: number, count: number): Promise { - 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 { - 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 - } - } -} diff --git a/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/interaction/syncDbWithBlockchain/UsersSync.role.ts b/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/interaction/syncDbWithBlockchain/UsersSync.role.ts index 03563468e..7f33a814d 100644 --- a/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/interaction/syncDbWithBlockchain/UsersSync.role.ts +++ b/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/interaction/syncDbWithBlockchain/UsersSync.role.ts @@ -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 { +export class UsersSyncRole extends AbstractSyncRole { + getDate(): Date { return this.peek().createdAt } @@ -14,18 +27,66 @@ export class UsersSyncRole extends AbstractSyncRole { return 'users' } - async loadFromDb(offset: number, count: number): Promise { - 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 { + 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 { + 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) + } } } diff --git a/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/interaction/syncDbWithBlockchain/syncDbWithBlockchain.context.ts b/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/interaction/syncDbWithBlockchain/syncDbWithBlockchain.context.ts index 53e0fcf39..e3b2d059f 100644 --- a/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/interaction/syncDbWithBlockchain/syncDbWithBlockchain.context.ts +++ b/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/interaction/syncDbWithBlockchain/syncDbWithBlockchain.context.ts @@ -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(', ')) - } + }*/ } diff --git a/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/valibot.schema.ts b/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/valibot.schema.ts index aa4a34685..581f77e3a 100644 --- a/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/valibot.schema.ts +++ b/dlt-connector/src/migrations/db-v2.7.0_to_blockchain-v3.5/valibot.schema.ts @@ -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 +export type CreationTransactionDb = v.InferOutput export type UserDb = v.InferOutput -export type CreatedUserDb = v.InferOutput export type TransactionLinkDb = v.InferOutput +export type RedeemedTransactionLinkDb = v.InferOutput +export type DeletedTransactionLinkDb = v.InferOutput export type CommunityDb = v.InferOutput -export type Balance = v.InferOutput export type CommunityContext = v.InferOutput