mirror of
https://github.com/IT4Change/gradido.git
synced 2026-04-25 23:37:36 +00:00
Merge pull request #3607 from gradido/migrate_with_zig
feat(dlt): migrate production db to blockchain
This commit is contained in:
commit
98c1b6ed10
4
.github/workflows/test_submodules.yml
vendored
4
.github/workflows/test_submodules.yml
vendored
@ -45,5 +45,7 @@ jobs:
|
|||||||
bun install --global turbo@^2
|
bun install --global turbo@^2
|
||||||
|
|
||||||
- name: typecheck, locales && unit test
|
- name: typecheck, locales && unit test
|
||||||
run: turbo core#test core#typecheck core#locales database#test database#typecheck shared#test shared#typecheck config-schema#test config-schema#typecheck
|
run: turbo core#typecheck core#locales database#test database#typecheck shared#test shared#typecheck config-schema#test config-schema#typecheck
|
||||||
|
- name: core test extra
|
||||||
|
run: turbo core#test
|
||||||
|
|
||||||
|
|||||||
@ -1,15 +1,21 @@
|
|||||||
import { CommunityAccountIdentifier } from './CommunityAccountIdentifier'
|
import { CommunityAccountIdentifier } from './CommunityAccountIdentifier'
|
||||||
export class AccountIdentifier {
|
export class AccountIdentifier {
|
||||||
communityTopicId: string
|
communityTopicId: string
|
||||||
|
communityId: string
|
||||||
account?: CommunityAccountIdentifier
|
account?: CommunityAccountIdentifier
|
||||||
seed?: string // used for deferred transfers
|
seed?: string // used for deferred transfers
|
||||||
|
|
||||||
constructor(communityTopicId: string, input: CommunityAccountIdentifier | string) {
|
constructor(
|
||||||
|
communityTopicId: string,
|
||||||
|
communityUuid: string,
|
||||||
|
input: CommunityAccountIdentifier | string,
|
||||||
|
) {
|
||||||
if (input instanceof CommunityAccountIdentifier) {
|
if (input instanceof CommunityAccountIdentifier) {
|
||||||
this.account = input
|
this.account = input
|
||||||
} else {
|
} else {
|
||||||
this.seed = input
|
this.seed = input
|
||||||
}
|
}
|
||||||
this.communityTopicId = communityTopicId
|
this.communityTopicId = communityTopicId
|
||||||
|
this.communityId = communityUuid
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
export class CommunityAccountIdentifier {
|
export class CommunityAccountIdentifier {
|
||||||
// for community user, uuid and communityUuid used
|
// for community user, uuid and communityUuid used
|
||||||
userUuid: string
|
userUuid: string
|
||||||
accountNr?: number
|
accountNr: number
|
||||||
|
|
||||||
constructor(userUuid: string, accountNr?: number) {
|
constructor(userUuid: string, accountNr: number = 1) {
|
||||||
this.userUuid = userUuid
|
this.userUuid = userUuid
|
||||||
this.accountNr = accountNr
|
this.accountNr = accountNr
|
||||||
}
|
}
|
||||||
|
|||||||
@ -36,6 +36,7 @@ export class TransactionDraft {
|
|||||||
const draft = new TransactionDraft()
|
const draft = new TransactionDraft()
|
||||||
draft.user = new AccountIdentifier(
|
draft.user = new AccountIdentifier(
|
||||||
community.hieroTopicId,
|
community.hieroTopicId,
|
||||||
|
community.communityUuid!,
|
||||||
new CommunityAccountIdentifier(user.gradidoID),
|
new CommunityAccountIdentifier(user.gradidoID),
|
||||||
)
|
)
|
||||||
draft.type = TransactionType.REGISTER_ADDRESS
|
draft.type = TransactionType.REGISTER_ADDRESS
|
||||||
@ -58,10 +59,12 @@ export class TransactionDraft {
|
|||||||
const draft = new TransactionDraft()
|
const draft = new TransactionDraft()
|
||||||
draft.user = new AccountIdentifier(
|
draft.user = new AccountIdentifier(
|
||||||
community.hieroTopicId,
|
community.hieroTopicId,
|
||||||
|
community.communityUuid!,
|
||||||
new CommunityAccountIdentifier(contribution.user.gradidoID),
|
new CommunityAccountIdentifier(contribution.user.gradidoID),
|
||||||
)
|
)
|
||||||
draft.linkedUser = new AccountIdentifier(
|
draft.linkedUser = new AccountIdentifier(
|
||||||
community.hieroTopicId,
|
community.hieroTopicId,
|
||||||
|
community.communityUuid!,
|
||||||
new CommunityAccountIdentifier(signingUser.gradidoID),
|
new CommunityAccountIdentifier(signingUser.gradidoID),
|
||||||
)
|
)
|
||||||
draft.type = TransactionType.GRADIDO_CREATION
|
draft.type = TransactionType.GRADIDO_CREATION
|
||||||
@ -96,10 +99,12 @@ export class TransactionDraft {
|
|||||||
const draft = new TransactionDraft()
|
const draft = new TransactionDraft()
|
||||||
draft.user = new AccountIdentifier(
|
draft.user = new AccountIdentifier(
|
||||||
senderUserTopic,
|
senderUserTopic,
|
||||||
|
sendingUser.community.communityUuid!,
|
||||||
new CommunityAccountIdentifier(sendingUser.gradidoID),
|
new CommunityAccountIdentifier(sendingUser.gradidoID),
|
||||||
)
|
)
|
||||||
draft.linkedUser = new AccountIdentifier(
|
draft.linkedUser = new AccountIdentifier(
|
||||||
receiverUserTopic,
|
receiverUserTopic,
|
||||||
|
receivingUser.community.communityUuid!,
|
||||||
new CommunityAccountIdentifier(receivingUser.gradidoID),
|
new CommunityAccountIdentifier(receivingUser.gradidoID),
|
||||||
)
|
)
|
||||||
draft.type = TransactionType.GRADIDO_TRANSFER
|
draft.type = TransactionType.GRADIDO_TRANSFER
|
||||||
@ -125,9 +130,14 @@ export class TransactionDraft {
|
|||||||
const draft = new TransactionDraft()
|
const draft = new TransactionDraft()
|
||||||
draft.user = new AccountIdentifier(
|
draft.user = new AccountIdentifier(
|
||||||
senderUserTopic,
|
senderUserTopic,
|
||||||
|
sendingUser.community.communityUuid!,
|
||||||
new CommunityAccountIdentifier(sendingUser.gradidoID),
|
new CommunityAccountIdentifier(sendingUser.gradidoID),
|
||||||
)
|
)
|
||||||
draft.linkedUser = new AccountIdentifier(senderUserTopic, transactionLink.code)
|
draft.linkedUser = new AccountIdentifier(
|
||||||
|
senderUserTopic,
|
||||||
|
sendingUser.community.communityUuid!,
|
||||||
|
transactionLink.code,
|
||||||
|
)
|
||||||
draft.type = TransactionType.GRADIDO_DEFERRED_TRANSFER
|
draft.type = TransactionType.GRADIDO_DEFERRED_TRANSFER
|
||||||
draft.createdAt = createdAtOnlySeconds.toISOString()
|
draft.createdAt = createdAtOnlySeconds.toISOString()
|
||||||
draft.amount = transactionLink.amount.toString()
|
draft.amount = transactionLink.amount.toString()
|
||||||
@ -159,9 +169,14 @@ export class TransactionDraft {
|
|||||||
const createdAtOnlySeconds = createdAt
|
const createdAtOnlySeconds = createdAt
|
||||||
createdAtOnlySeconds.setMilliseconds(0)
|
createdAtOnlySeconds.setMilliseconds(0)
|
||||||
const draft = new TransactionDraft()
|
const draft = new TransactionDraft()
|
||||||
draft.user = new AccountIdentifier(senderUserTopic, transactionLink.code)
|
draft.user = new AccountIdentifier(
|
||||||
|
senderUserTopic,
|
||||||
|
transactionLink.user.community.communityUuid!,
|
||||||
|
transactionLink.code,
|
||||||
|
)
|
||||||
draft.linkedUser = new AccountIdentifier(
|
draft.linkedUser = new AccountIdentifier(
|
||||||
recipientUserTopic,
|
recipientUserTopic,
|
||||||
|
recipientUser.community.communityUuid!,
|
||||||
new CommunityAccountIdentifier(recipientUser.gradidoID),
|
new CommunityAccountIdentifier(recipientUser.gradidoID),
|
||||||
)
|
)
|
||||||
draft.type = TransactionType.GRADIDO_REDEEM_DEFERRED_TRANSFER
|
draft.type = TransactionType.GRADIDO_REDEEM_DEFERRED_TRANSFER
|
||||||
|
|||||||
@ -8,6 +8,7 @@ export const getPublicCommunityInfo = gql`
|
|||||||
creationDate
|
creationDate
|
||||||
publicKey
|
publicKey
|
||||||
publicJwtKey
|
publicJwtKey
|
||||||
|
hieroTopicId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|||||||
@ -2,7 +2,12 @@ import { Paginated } from '@arg/Paginated'
|
|||||||
import { EditCommunityInput } from '@input/EditCommunityInput'
|
import { EditCommunityInput } from '@input/EditCommunityInput'
|
||||||
import { AdminCommunityView } from '@model/AdminCommunityView'
|
import { AdminCommunityView } from '@model/AdminCommunityView'
|
||||||
import { Community } from '@model/Community'
|
import { Community } from '@model/Community'
|
||||||
import { Community as DbCommunity, getHomeCommunity, getReachableCommunities } from 'database'
|
import {
|
||||||
|
Community as DbCommunity,
|
||||||
|
getAuthorizedCommunities,
|
||||||
|
getHomeCommunity,
|
||||||
|
getReachableCommunities,
|
||||||
|
} from 'database'
|
||||||
import { updateAllDefinedAndChanged } from 'shared'
|
import { updateAllDefinedAndChanged } from 'shared'
|
||||||
import { Arg, Args, Authorized, Mutation, Query, Resolver } from 'type-graphql'
|
import { Arg, Args, Authorized, Mutation, Query, Resolver } from 'type-graphql'
|
||||||
import { RIGHTS } from '@/auth/RIGHTS'
|
import { RIGHTS } from '@/auth/RIGHTS'
|
||||||
@ -34,6 +39,17 @@ export class CommunityResolver {
|
|||||||
return dbCommunities.map((dbCom: DbCommunity) => new Community(dbCom))
|
return dbCommunities.map((dbCom: DbCommunity) => new Community(dbCom))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Authorized([RIGHTS.COMMUNITIES])
|
||||||
|
@Query(() => [Community])
|
||||||
|
async authorizedCommunities(): Promise<Community[]> {
|
||||||
|
const dbCommunities: DbCommunity[] = await getAuthorizedCommunities({
|
||||||
|
// order by
|
||||||
|
foreign: 'ASC', // home community first
|
||||||
|
name: 'ASC',
|
||||||
|
})
|
||||||
|
return dbCommunities.map((dbCom: DbCommunity) => new Community(dbCom))
|
||||||
|
}
|
||||||
|
|
||||||
@Authorized([RIGHTS.COMMUNITIES])
|
@Authorized([RIGHTS.COMMUNITIES])
|
||||||
@Query(() => Community)
|
@Query(() => Community)
|
||||||
async communityByIdentifier(
|
async communityByIdentifier(
|
||||||
|
|||||||
@ -86,6 +86,7 @@ async function clearDatabase(db: AppDatabase) {
|
|||||||
await trx.query(`SET FOREIGN_KEY_CHECKS = 0`)
|
await trx.query(`SET FOREIGN_KEY_CHECKS = 0`)
|
||||||
await trx.query(`TRUNCATE TABLE contributions`)
|
await trx.query(`TRUNCATE TABLE contributions`)
|
||||||
await trx.query(`TRUNCATE TABLE contribution_links`)
|
await trx.query(`TRUNCATE TABLE contribution_links`)
|
||||||
|
await trx.query(`TRUNCATE TABLE events`)
|
||||||
await trx.query(`TRUNCATE TABLE users`)
|
await trx.query(`TRUNCATE TABLE users`)
|
||||||
await trx.query(`TRUNCATE TABLE user_contacts`)
|
await trx.query(`TRUNCATE TABLE user_contacts`)
|
||||||
await trx.query(`TRUNCATE TABLE user_roles`)
|
await trx.query(`TRUNCATE TABLE user_roles`)
|
||||||
|
|||||||
34
bun.lock
34
bun.lock
@ -7,7 +7,7 @@
|
|||||||
"auto-changelog": "^2.4.0",
|
"auto-changelog": "^2.4.0",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"jose": "^4.14.4",
|
"jose": "^4.14.4",
|
||||||
"turbo": "^2.5.0",
|
"turbo": "^2.8.12",
|
||||||
"uuid": "^8.3.2",
|
"uuid": "^8.3.2",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@ -18,7 +18,7 @@
|
|||||||
},
|
},
|
||||||
"admin": {
|
"admin": {
|
||||||
"name": "admin",
|
"name": "admin",
|
||||||
"version": "2.7.3",
|
"version": "2.7.4",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@iconify/json": "^2.2.228",
|
"@iconify/json": "^2.2.228",
|
||||||
"@popperjs/core": "^2.11.8",
|
"@popperjs/core": "^2.11.8",
|
||||||
@ -88,7 +88,7 @@
|
|||||||
},
|
},
|
||||||
"backend": {
|
"backend": {
|
||||||
"name": "backend",
|
"name": "backend",
|
||||||
"version": "2.7.3",
|
"version": "2.7.4",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"email-templates": "^10.0.1",
|
"email-templates": "^10.0.1",
|
||||||
@ -165,7 +165,7 @@
|
|||||||
},
|
},
|
||||||
"config-schema": {
|
"config-schema": {
|
||||||
"name": "config-schema",
|
"name": "config-schema",
|
||||||
"version": "2.7.3",
|
"version": "2.7.4",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.25.2",
|
"esbuild": "^0.25.2",
|
||||||
"joi": "17.13.3",
|
"joi": "17.13.3",
|
||||||
@ -183,7 +183,7 @@
|
|||||||
},
|
},
|
||||||
"core": {
|
"core": {
|
||||||
"name": "core",
|
"name": "core",
|
||||||
"version": "2.7.3",
|
"version": "2.7.4",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"database": "*",
|
"database": "*",
|
||||||
"email-templates": "^10.0.1",
|
"email-templates": "^10.0.1",
|
||||||
@ -220,7 +220,7 @@
|
|||||||
},
|
},
|
||||||
"database": {
|
"database": {
|
||||||
"name": "database",
|
"name": "database",
|
||||||
"version": "2.7.3",
|
"version": "2.7.4",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/uuid": "^8.3.4",
|
"@types/uuid": "^8.3.4",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
@ -256,7 +256,7 @@
|
|||||||
},
|
},
|
||||||
"dht-node": {
|
"dht-node": {
|
||||||
"name": "dht-node",
|
"name": "dht-node",
|
||||||
"version": "2.7.3",
|
"version": "2.7.4",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"dht-rpc": "6.18.1",
|
"dht-rpc": "6.18.1",
|
||||||
@ -294,7 +294,7 @@
|
|||||||
},
|
},
|
||||||
"federation": {
|
"federation": {
|
||||||
"name": "federation",
|
"name": "federation",
|
||||||
"version": "2.7.3",
|
"version": "2.7.4",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"email-templates": "^10.0.1",
|
"email-templates": "^10.0.1",
|
||||||
@ -355,7 +355,7 @@
|
|||||||
},
|
},
|
||||||
"frontend": {
|
"frontend": {
|
||||||
"name": "frontend",
|
"name": "frontend",
|
||||||
"version": "2.7.3",
|
"version": "2.7.4",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@morev/vue-transitions": "^3.0.2",
|
"@morev/vue-transitions": "^3.0.2",
|
||||||
"@types/leaflet": "^1.9.12",
|
"@types/leaflet": "^1.9.12",
|
||||||
@ -451,7 +451,7 @@
|
|||||||
},
|
},
|
||||||
"shared": {
|
"shared": {
|
||||||
"name": "shared",
|
"name": "shared",
|
||||||
"version": "2.7.3",
|
"version": "2.7.4",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"decimal.js-light": "^2.5.1",
|
"decimal.js-light": "^2.5.1",
|
||||||
"esbuild": "^0.25.2",
|
"esbuild": "^0.25.2",
|
||||||
@ -3357,19 +3357,19 @@
|
|||||||
|
|
||||||
"tunnel": ["tunnel@0.0.6", "", {}, "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg=="],
|
"tunnel": ["tunnel@0.0.6", "", {}, "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg=="],
|
||||||
|
|
||||||
"turbo": ["turbo@2.6.1", "", { "optionalDependencies": { "turbo-darwin-64": "2.6.1", "turbo-darwin-arm64": "2.6.1", "turbo-linux-64": "2.6.1", "turbo-linux-arm64": "2.6.1", "turbo-windows-64": "2.6.1", "turbo-windows-arm64": "2.6.1" }, "bin": { "turbo": "bin/turbo" } }, "sha512-qBwXXuDT3rA53kbNafGbT5r++BrhRgx3sAo0cHoDAeG9g1ItTmUMgltz3Hy7Hazy1ODqNpR+C7QwqL6DYB52yA=="],
|
"turbo": ["turbo@2.8.12", "", { "optionalDependencies": { "turbo-darwin-64": "2.8.12", "turbo-darwin-arm64": "2.8.12", "turbo-linux-64": "2.8.12", "turbo-linux-arm64": "2.8.12", "turbo-windows-64": "2.8.12", "turbo-windows-arm64": "2.8.12" }, "bin": { "turbo": "bin/turbo" } }, "sha512-auUAMLmi0eJhxDhQrxzvuhfEbICnVt0CTiYQYY8WyRJ5nwCDZxD0JG8bCSxT4nusI2CwJzmZAay5BfF6LmK7Hw=="],
|
||||||
|
|
||||||
"turbo-darwin-64": ["turbo-darwin-64@2.6.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-Dm0HwhyZF4J0uLqkhUyCVJvKM9Rw7M03v3J9A7drHDQW0qAbIGBrUijQ8g4Q9Cciw/BXRRd8Uzkc3oue+qn+ZQ=="],
|
"turbo-darwin-64": ["turbo-darwin-64@2.8.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-EiHJmW2MeQQx+21x8hjMHw/uPhXt9PIxvDrxzOtyVwrXzL0tQmsxtO4qHf2l7uA+K6PUJ4+TjY1MHZDuCvWXrw=="],
|
||||||
|
|
||||||
"turbo-darwin-arm64": ["turbo-darwin-arm64@2.6.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-U0PIPTPyxdLsrC3jN7jaJUwgzX5sVUBsKLO7+6AL+OASaa1NbT1pPdiZoTkblBAALLP76FM0LlnsVQOnmjYhyw=="],
|
"turbo-darwin-arm64": ["turbo-darwin-arm64@2.8.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-cbqqGN0vd7ly2TeuaM8k9AK9u1CABO4kBA5KPSqovTiLL3sORccn/mZzJSbvQf0EsYRfU34MgW5FotfwW3kx8Q=="],
|
||||||
|
|
||||||
"turbo-linux-64": ["turbo-linux-64@2.6.1", "", { "os": "linux", "cpu": "x64" }, "sha512-eM1uLWgzv89bxlK29qwQEr9xYWBhmO/EGiH22UGfq+uXr+QW1OvNKKMogSN65Ry8lElMH4LZh0aX2DEc7eC0Mw=="],
|
"turbo-linux-64": ["turbo-linux-64@2.8.12", "", { "os": "linux", "cpu": "x64" }, "sha512-jXKw9j4r4q6s0goSXuKI3aKbQK2qiNeP25lGGEnq018TM6SWRW1CCpPMxyG91aCKrub7wDm/K45sGNT4ZFBcFQ=="],
|
||||||
|
|
||||||
"turbo-linux-arm64": ["turbo-linux-arm64@2.6.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-MFFh7AxAQAycXKuZDrbeutfWM5Ep0CEZ9u7zs4Hn2FvOViTCzIfEhmuJou3/a5+q5VX1zTxQrKGy+4Lf5cdpsA=="],
|
"turbo-linux-arm64": ["turbo-linux-arm64@2.8.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-BRJCMdyXjyBoL0GYpvj9d2WNfMHwc3tKmJG5ATn2Efvil9LsiOsd/93/NxDqW0jACtHFNVOPnd/CBwXRPiRbwA=="],
|
||||||
|
|
||||||
"turbo-windows-64": ["turbo-windows-64@2.6.1", "", { "os": "win32", "cpu": "x64" }, "sha512-buq7/VAN7KOjMYi4tSZT5m+jpqyhbRU2EUTTvp6V0Ii8dAkY2tAAjQN1q5q2ByflYWKecbQNTqxmVploE0LVwQ=="],
|
"turbo-windows-64": ["turbo-windows-64@2.8.12", "", { "os": "win32", "cpu": "x64" }, "sha512-vyFOlpFFzQFkikvSVhVkESEfzIopgs2J7J1rYvtSwSHQ4zmHxkC95Q8Kjkus8gg+8X2mZyP1GS5jirmaypGiPw=="],
|
||||||
|
|
||||||
"turbo-windows-arm64": ["turbo-windows-arm64@2.6.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-7w+AD5vJp3R+FB0YOj1YJcNcOOvBior7bcHTodqp90S3x3bLgpr7tE6xOea1e8JkP7GK6ciKVUpQvV7psiwU5Q=="],
|
"turbo-windows-arm64": ["turbo-windows-arm64@2.8.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-9nRnlw5DF0LkJClkIws1evaIF36dmmMEO84J5Uj4oQ8C0QTHwlH7DNe5Kq2Jdmu8GXESCNDNuUYG8Cx6W/vm3g=="],
|
||||||
|
|
||||||
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
|
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,345 @@
|
|||||||
|
import Decimal from 'decimal.js-light'
|
||||||
|
import { DECAY_FACTOR, reverseLegacyDecay } from 'shared'
|
||||||
|
|
||||||
|
function calculateEffectiveSeconds(holdOriginal: Decimal, holdCorrected: Decimal): Decimal {
|
||||||
|
return holdOriginal.div(holdCorrected).ln().div(DECAY_FACTOR.ln())
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
|
||||||
|
/**
|
||||||
|
* Migration: Correct historical inconsistencies in transactions, users, and contribution_links.
|
||||||
|
*
|
||||||
|
* Background:
|
||||||
|
* Early Gradido production data contains several inconsistencies that violate
|
||||||
|
* stricter blockchain validation rules. These inconsistencies include:
|
||||||
|
* - Contribution transactions confirmed by users who did not exist at the
|
||||||
|
* time of the transaction.
|
||||||
|
* - Users confirming their own contributions (self-signed transactions).
|
||||||
|
* - Users whose `created_at` timestamp is after or equal to their first
|
||||||
|
* transaction.
|
||||||
|
* - Transaction memos shorter than the required minimum length (5 characters).
|
||||||
|
* - Existing contribution_links without an associated 'ADMIN_CONTRIBUTION_LINK_CREATE' event,
|
||||||
|
* which is used to find someone who confirmed the contribution.
|
||||||
|
*
|
||||||
|
* Purpose:
|
||||||
|
* This migration performs the following corrections to ensure historical
|
||||||
|
* consistency and full compatibility with blockchain validation rules:
|
||||||
|
* 1. Fix self-signed contributions by assigning the actual moderator.
|
||||||
|
* 2. Replace invalid moderators with the earliest ADMIN or MODERATOR where
|
||||||
|
* the linked user was created after the transaction.
|
||||||
|
* 3. Update user creation dates to be before their first transaction.
|
||||||
|
* 4. Ensure all transaction memos meet the minimum length requirement.
|
||||||
|
* 5. Insert missing 'ADMIN_CONTRIBUTION_LINK_CREATE' events for contribution_links
|
||||||
|
* that do not yet have such events, using the first Admin as acting_user.
|
||||||
|
*
|
||||||
|
* Outcome:
|
||||||
|
* After this migration:
|
||||||
|
* - All contribution transactions reference a valid moderator existing at the time of the transaction.
|
||||||
|
* - User creation dates are logically consistent with their transactions.
|
||||||
|
* - Transaction memos meet the minimum formatting rules.
|
||||||
|
* - Every contribution_link has a corresponding 'ADMIN_CONTRIBUTION_LINK_CREATE' event,
|
||||||
|
* ensuring blockchain consistency for contributions.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fix 0: Update transaction links to match holdAvailableAmount with validUntil, because the old formula lead to incorrect values
|
||||||
|
*/
|
||||||
|
|
||||||
|
let count = 0
|
||||||
|
let lastProcessedId = 0
|
||||||
|
const LIMIT = 200
|
||||||
|
do {
|
||||||
|
const rows = await queryFn(
|
||||||
|
`
|
||||||
|
SELECT id, amount, hold_available_amount, validUntil, createdAt, redeemedAt, deletedAt
|
||||||
|
FROM transaction_links
|
||||||
|
WHERE id > ?
|
||||||
|
ORDER BY id ASC
|
||||||
|
LIMIT ?
|
||||||
|
`,
|
||||||
|
[lastProcessedId, LIMIT],
|
||||||
|
)
|
||||||
|
if (!rows.length) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
const updates: Array<{ id: number; newValidUntil: string }> = []
|
||||||
|
for (const row of rows) {
|
||||||
|
const validUntil = new Date(row.validUntil)
|
||||||
|
const redeemedAt = row.redeemedAt ? new Date(row.redeemedAt) : null
|
||||||
|
const deletedAt = row.deletedAt ? new Date(row.deletedAt) : null
|
||||||
|
const createdAt = new Date(row.createdAt)
|
||||||
|
const amount = new Decimal(row.amount)
|
||||||
|
const duration = (validUntil.getTime() - createdAt.getTime()) / 1000
|
||||||
|
const blockedAmountCorrected = reverseLegacyDecay(amount, duration)
|
||||||
|
// fix only if the difference is big enough to have an impact
|
||||||
|
if (blockedAmountCorrected.sub(amount).abs().lt(new Decimal('0.001'))) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const holdAvailableAmount = new Decimal(row.hold_available_amount)
|
||||||
|
const secondsDiff = calculateEffectiveSeconds(
|
||||||
|
new Decimal(holdAvailableAmount.toString()),
|
||||||
|
new Decimal(blockedAmountCorrected.toString()),
|
||||||
|
)
|
||||||
|
const newValidUntil = new Date(validUntil.getTime() - secondsDiff.mul(1000).toNumber())
|
||||||
|
if (
|
||||||
|
(redeemedAt && redeemedAt.getTime() < validUntil.getTime()) ||
|
||||||
|
(deletedAt && deletedAt.getTime() < validUntil.getTime())
|
||||||
|
) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
updates.push({
|
||||||
|
id: row.id,
|
||||||
|
newValidUntil: newValidUntil.toISOString().replace('T', ' ').replace('Z', ''),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (updates.length > 0) {
|
||||||
|
const caseStatements = updates.map((u) => `WHEN ${u.id} THEN '${u.newValidUntil}'`).join('\n')
|
||||||
|
|
||||||
|
await queryFn(
|
||||||
|
`
|
||||||
|
UPDATE transaction_links
|
||||||
|
SET validUntil = CASE id
|
||||||
|
${caseStatements}
|
||||||
|
END
|
||||||
|
WHERE id IN (?)
|
||||||
|
`,
|
||||||
|
[updates.map((u) => u.id)],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
count = rows.length
|
||||||
|
lastProcessedId = rows[rows.length - 1].id
|
||||||
|
} while (count === LIMIT)
|
||||||
|
///*/
|
||||||
|
/**
|
||||||
|
* Fix 1: Remove self-signed contributions.
|
||||||
|
*
|
||||||
|
* Background:
|
||||||
|
* A core rule in the system states that *no user may confirm their own
|
||||||
|
* contribution* — a moderator must always be someone else.
|
||||||
|
*
|
||||||
|
* However, early production data contains transactions where the `linked_user_id`
|
||||||
|
* matches the `user_id`, meaning the contributor confirmed their own contribution.
|
||||||
|
*
|
||||||
|
* This query corrects those records by replacing the `linked_user` with the
|
||||||
|
* moderator stored in `contributions.moderator_id`.
|
||||||
|
*
|
||||||
|
* Only transactions where:
|
||||||
|
* - the type is a contribution (type_id = 1),
|
||||||
|
* - the linked user equals the contributor (`t.user_id = t.linked_user_id`),
|
||||||
|
* - the moderator existed before the time of the transaction,
|
||||||
|
* - and the moderator is not the same person,
|
||||||
|
* are updated.
|
||||||
|
*/
|
||||||
|
await queryFn(`
|
||||||
|
UPDATE transactions t
|
||||||
|
JOIN contributions c ON(t.id = c.transaction_id)
|
||||||
|
JOIN users u ON(c.moderator_id = u.id)
|
||||||
|
SET t.linked_user_id = u.id,
|
||||||
|
t.linked_user_community_uuid = u.community_uuid,
|
||||||
|
t.linked_user_gradido_id = u.gradido_id,
|
||||||
|
t.linked_user_name = CONCAT(u.first_name, ' ', u.last_name)
|
||||||
|
WHERE t.type_id = 1
|
||||||
|
AND t.user_id = t.linked_user_id
|
||||||
|
AND u.created_at < t.balance_date
|
||||||
|
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.
|
||||||
|
*
|
||||||
|
* Background:
|
||||||
|
* Early production records contain contribution transactions where the assigned
|
||||||
|
* moderator ("linked_user" or "contribution.moderator_id") was created *after* the contribution itself. This
|
||||||
|
* is invalid in the blockchain verification process, which requires that the
|
||||||
|
* moderator account must have existed *before* the time of the transaction.
|
||||||
|
*
|
||||||
|
* This migration:
|
||||||
|
* 1. Identifies the earliest ADMIN or MODERATOR user in the system.
|
||||||
|
* 2. Reassigns them as moderator for all affected transactions where:
|
||||||
|
* - the type is a contribution (type_id = 1),
|
||||||
|
* - the linked user was created after or at the transaction date,
|
||||||
|
* - the transaction occurred after the ADMIN’s or MODERATOR's creation,
|
||||||
|
* - and the contributor is not the ADMIN or MODERATOR.
|
||||||
|
*
|
||||||
|
* Using the earliest ADMIN or MODERATOR ensures:
|
||||||
|
* - historical consistency,
|
||||||
|
* - minimal intrusion,
|
||||||
|
* - and compatibility with blockchain validation rules.
|
||||||
|
*/
|
||||||
|
await queryFn(`
|
||||||
|
UPDATE transactions t
|
||||||
|
JOIN (
|
||||||
|
SELECT t_sub.id as sub_t_id, u_sub.created_at, u_sub.id, u_sub.community_uuid, u_sub.gradido_id, CONCAT(u_sub.first_name, ' ', u_sub.last_name) AS linked_user_name
|
||||||
|
FROM transactions t_sub
|
||||||
|
JOIN users u_sub on(t_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 t_sub.id
|
||||||
|
ORDER BY r_sub.created_at ASC
|
||||||
|
) moderator ON (t.id = moderator.sub_t_id)
|
||||||
|
LEFT JOIN users u on(t.linked_user_id = u.id)
|
||||||
|
SET t.linked_user_id = moderator.id,
|
||||||
|
t.linked_user_community_uuid = moderator.community_uuid,
|
||||||
|
t.linked_user_gradido_id = moderator.gradido_id,
|
||||||
|
t.linked_user_name = moderator.linked_user_name
|
||||||
|
WHERE t.type_id = 1
|
||||||
|
AND t.balance_date <= u.created_at
|
||||||
|
AND t.balance_date > moderator.created_at
|
||||||
|
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.
|
||||||
|
*
|
||||||
|
* Background:
|
||||||
|
* In early production data, some users have a `created_at` timestamp that is
|
||||||
|
* **after or equal** to their first recorded transaction (`balance_date`).
|
||||||
|
* This violates logical consistency, because a user cannot exist *after* their
|
||||||
|
* own transaction.
|
||||||
|
*
|
||||||
|
* What this query does:
|
||||||
|
* - For each user, it finds the earliest transaction date (`first_date`) from
|
||||||
|
* the `transactions` table.
|
||||||
|
* - It updates the user's `created_at` timestamp to **1 second before** their
|
||||||
|
* first transaction.
|
||||||
|
*
|
||||||
|
* Notes:
|
||||||
|
* - Only users where `created_at >= first transaction date` are affected.
|
||||||
|
* - This is a historical data fix to ensure all transactions reference a user
|
||||||
|
* that already exists at the time of the transaction, which is required for
|
||||||
|
* blockchain validation and logical consistency in the system.
|
||||||
|
*/
|
||||||
|
await queryFn(`
|
||||||
|
UPDATE users u
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT user_id, MIN(balance_date) AS first_date
|
||||||
|
FROM transactions
|
||||||
|
GROUP BY user_id
|
||||||
|
) t ON t.user_id = u.id
|
||||||
|
SET u.created_at = DATE_SUB(t.first_date, INTERVAL 1 SECOND)
|
||||||
|
WHERE u.created_at >= t.first_date;
|
||||||
|
;`)
|
||||||
|
|
||||||
|
// linked user also, but we need to use gradido_id as index, because on cross group transactions linked_user_id is empty
|
||||||
|
await queryFn(`
|
||||||
|
UPDATE users u
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT linked_user_gradido_id , MIN(balance_date) AS first_date
|
||||||
|
FROM transactions
|
||||||
|
GROUP BY linked_user_gradido_id
|
||||||
|
) t ON t.linked_user_gradido_id = u.gradido_id
|
||||||
|
SET u.created_at = DATE_SUB(t.first_date, INTERVAL 1 SECOND)
|
||||||
|
WHERE u.created_at >= t.first_date;
|
||||||
|
;`)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fix 4: Ensure all transaction memos meet the minimum length requirement.
|
||||||
|
*
|
||||||
|
* Background:
|
||||||
|
* In early Gradido production data, some transactions have a `memo` field
|
||||||
|
* shorter than the current rule of 5 characters. This can cause issues in
|
||||||
|
* reporting, display, or blockchain validation processes that expect
|
||||||
|
* a minimum memo length.
|
||||||
|
*
|
||||||
|
* What this query does:
|
||||||
|
* - For memos with 0 characters, sets the value to 'empty empty'.
|
||||||
|
* - For memos with 1-4 characters, pads the memo on the left with spaces
|
||||||
|
* until it reaches 5 characters.
|
||||||
|
* - Memos that are already 5 characters or longer are left unchanged.
|
||||||
|
*
|
||||||
|
* Notes:
|
||||||
|
* - This ensures all memos are at least 5 characters long.
|
||||||
|
* - The padding uses spaces.
|
||||||
|
* - Only memos shorter than 5 characters are affected.
|
||||||
|
*/
|
||||||
|
await queryFn(`
|
||||||
|
UPDATE transactions 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 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.
|
||||||
|
*
|
||||||
|
* Background:
|
||||||
|
* Each contribution in the blockchain requires a confirmation by a user.
|
||||||
|
* In the current DB version, there is no information about who confirmed contributions based on contribution_links.
|
||||||
|
* Recently, functionality was added to create an 'ADMIN_CONTRIBUTION_LINK_CREATE' event
|
||||||
|
* for newly created contribution_links, but existing contribution_links were not updated.
|
||||||
|
*
|
||||||
|
* This query inserts an 'ADMIN_CONTRIBUTION_LINK_CREATE' event for every contribution_link
|
||||||
|
* that does not already have such an event.
|
||||||
|
* The acting_user_id is set to the first Admin, and affected_user_id is set to 0.
|
||||||
|
*/
|
||||||
|
await queryFn(`
|
||||||
|
INSERT INTO \`events\`(acting_user_id, affected_user_id, \`type\`, involved_contribution_link_id)
|
||||||
|
SELECT (
|
||||||
|
SELECT u.id
|
||||||
|
FROM users u
|
||||||
|
JOIN user_roles r ON r.user_id = u.id
|
||||||
|
WHERE r.role = 'ADMIN'
|
||||||
|
ORDER BY r.id ASC
|
||||||
|
LIMIT 1
|
||||||
|
) AS acting_user_id, 0 as affected_user_id, 'ADMIN_CONTRIBUTION_LINK_CREATE' AS \`type\`, c.id AS involved_contribution_link_id
|
||||||
|
FROM contribution_links c
|
||||||
|
LEFT JOIN \`events\` e ON e.involved_contribution_link_id = c.id AND e.type = 'ADMIN_CONTRIBUTION_LINK_CREATE'
|
||||||
|
WHERE e.id IS NULL
|
||||||
|
;`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
|
||||||
|
// downgrade not possible
|
||||||
|
}
|
||||||
@ -84,7 +84,7 @@ export async function getReachableCommunities(
|
|||||||
federatedCommunities: {
|
federatedCommunities: {
|
||||||
verifiedAt: MoreThanOrEqual(new Date(Date.now() - authenticationTimeoutMs)),
|
verifiedAt: MoreThanOrEqual(new Date(Date.now() - authenticationTimeoutMs)),
|
||||||
},
|
},
|
||||||
},
|
}, // or
|
||||||
{ foreign: false },
|
{ foreign: false },
|
||||||
],
|
],
|
||||||
order,
|
order,
|
||||||
@ -99,3 +99,16 @@ export async function getNotReachableCommunities(
|
|||||||
order,
|
order,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// return the home community and all communities which had at least once make it through the first handshake
|
||||||
|
export async function getAuthorizedCommunities(
|
||||||
|
order?: FindOptionsOrder<DbCommunity>,
|
||||||
|
): Promise<DbCommunity[]> {
|
||||||
|
return await DbCommunity.find({
|
||||||
|
where: [
|
||||||
|
{ authenticatedAt: Not(IsNull()) }, // or
|
||||||
|
{ foreign: false },
|
||||||
|
],
|
||||||
|
order,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
2
dlt-connector/.gitignore
vendored
2
dlt-connector/.gitignore
vendored
@ -3,6 +3,8 @@
|
|||||||
/.env.bak
|
/.env.bak
|
||||||
/build/
|
/build/
|
||||||
/locales/
|
/locales/
|
||||||
|
lib
|
||||||
|
.zigar-cache
|
||||||
package-json.lock
|
package-json.lock
|
||||||
coverage
|
coverage
|
||||||
# emacs
|
# emacs
|
||||||
|
|||||||
@ -4,7 +4,9 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "dlt-connector",
|
"name": "dlt-connector",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"gradido-blockchain-js": "git+https://github.com/gradido/gradido-blockchain-js#f265dbb1780a912cf8b0418dfe3eaf5cdc5b51cf",
|
"bun-zigar": "^0.15.2",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
|
"gradido-blockchain-js": "git+https://github.com/gradido/gradido-blockchain-js#785fd766289726d41ae01f1e80a274aed871a7fb",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "2.0.0",
|
"@biomejs/biome": "2.0.0",
|
||||||
@ -18,6 +20,7 @@
|
|||||||
"@types/uuid": "^8.3.4",
|
"@types/uuid": "^8.3.4",
|
||||||
"adm-zip": "^0.5.16",
|
"adm-zip": "^0.5.16",
|
||||||
"async-mutex": "^0.5.0",
|
"async-mutex": "^0.5.0",
|
||||||
|
"decimal.js-light": "^2.5.1",
|
||||||
"dotenv": "^10.0.0",
|
"dotenv": "^10.0.0",
|
||||||
"drizzle-orm": "^0.44.7",
|
"drizzle-orm": "^0.44.7",
|
||||||
"elysia": "1.3.8",
|
"elysia": "1.3.8",
|
||||||
@ -389,6 +392,8 @@
|
|||||||
|
|
||||||
"bun-types": ["bun-types@1.3.1", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-NMrcy7smratanWJ2mMXdpatalovtxVggkj11bScuWuiOoXTiKIu2eVS1/7qbyI/4yHedtsn175n4Sm4JcdHLXw=="],
|
"bun-types": ["bun-types@1.3.1", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-NMrcy7smratanWJ2mMXdpatalovtxVggkj11bScuWuiOoXTiKIu2eVS1/7qbyI/4yHedtsn175n4Sm4JcdHLXw=="],
|
||||||
|
|
||||||
|
"bun-zigar": ["bun-zigar@0.15.2", "", { "dependencies": { "node-zigar-addon": "0.15.2", "zigar-compiler": "^0.15.2" }, "bin": { "zigar": "bin/cli.js", "bun-zigar": "bin/cli.js" } }, "sha512-slEHTEapQEIqB86OeiToPuuFXe39DCIYISTPzbIMBTZL34vRzCIa5wFn5ATudauHFFwl5/y5JYv8tluk2QL9Eg=="],
|
||||||
|
|
||||||
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
|
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
|
||||||
|
|
||||||
"camelcase": ["camelcase@6.3.0", "", {}, "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA=="],
|
"camelcase": ["camelcase@6.3.0", "", {}, "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA=="],
|
||||||
@ -431,6 +436,8 @@
|
|||||||
|
|
||||||
"cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="],
|
"cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="],
|
||||||
|
|
||||||
|
"cross-env": ["cross-env@7.0.3", "", { "dependencies": { "cross-spawn": "^7.0.1" }, "bin": { "cross-env": "src/bin/cross-env.js", "cross-env-shell": "src/bin/cross-env-shell.js" } }, "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw=="],
|
||||||
|
|
||||||
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
||||||
|
|
||||||
"crypto-js": ["crypto-js@4.2.0", "", {}, "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="],
|
"crypto-js": ["crypto-js@4.2.0", "", {}, "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="],
|
||||||
@ -443,6 +450,8 @@
|
|||||||
|
|
||||||
"debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
|
"debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
|
||||||
|
|
||||||
|
"decimal.js-light": ["decimal.js-light@2.5.1", "", {}, "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="],
|
||||||
|
|
||||||
"deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="],
|
"deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="],
|
||||||
|
|
||||||
"delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
|
"delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
|
||||||
@ -575,7 +584,7 @@
|
|||||||
|
|
||||||
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||||
|
|
||||||
"gradido-blockchain-js": ["gradido-blockchain-js@github:gradido/gradido-blockchain-js#f265dbb", { "dependencies": { "bindings": "^1.5.0", "nan": "^2.20.0", "node-addon-api": "^7.1.1", "node-gyp-build": "^4.8.1", "prebuildify": "git+https://github.com/einhornimmond/prebuildify#65d94455fab86b902c0d59bb9c06ac70470e56b2" } }, "gradido-gradido-blockchain-js-f265dbb"],
|
"gradido-blockchain-js": ["gradido-blockchain-js@github:gradido/gradido-blockchain-js#785fd76", { "dependencies": { "bindings": "^1.5.0", "nan": "^2.20.0", "node-addon-api": "^7.1.1", "node-gyp-build": "^4.8.1", "prebuildify": "git+https://github.com/einhornimmond/prebuildify#65d94455fab86b902c0d59bb9c06ac70470e56b2" } }, "gradido-gradido-blockchain-js-785fd76"],
|
||||||
|
|
||||||
"graphql": ["graphql@16.11.0", "", {}, "sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw=="],
|
"graphql": ["graphql@16.11.0", "", {}, "sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw=="],
|
||||||
|
|
||||||
@ -777,7 +786,7 @@
|
|||||||
|
|
||||||
"node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="],
|
"node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="],
|
||||||
|
|
||||||
"node-api-headers": ["node-api-headers@1.6.0", "", {}, "sha512-81T99+mWLZnxX0LlZPYuafyFlxVVaWKQ0BDAbSrOqLO+v+gzCzu0GTAVNeVK8lucqjqo9L/1UcK9cpkem8Py4Q=="],
|
"node-api-headers": ["node-api-headers@1.8.0", "", {}, "sha512-jfnmiKWjRAGbdD1yQS28bknFM1tbHC1oucyuMPjmkEs+kpiu76aRs40WlTmBmyEgzDM76ge1DQ7XJ3R5deiVjQ=="],
|
||||||
|
|
||||||
"node-gyp-build": ["node-gyp-build@4.8.4", "", { "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", "node-gyp-build-test": "build-test.js" } }, "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ=="],
|
"node-gyp-build": ["node-gyp-build@4.8.4", "", { "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", "node-gyp-build-test": "build-test.js" } }, "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ=="],
|
||||||
|
|
||||||
@ -785,6 +794,8 @@
|
|||||||
|
|
||||||
"node-releases": ["node-releases@2.0.26", "", {}, "sha512-S2M9YimhSjBSvYnlr5/+umAnPHE++ODwt5e2Ij6FoX45HA/s4vHdkDx1eax2pAPeAOqu4s9b7ppahsyEFdVqQA=="],
|
"node-releases": ["node-releases@2.0.26", "", {}, "sha512-S2M9YimhSjBSvYnlr5/+umAnPHE++ODwt5e2Ij6FoX45HA/s4vHdkDx1eax2pAPeAOqu4s9b7ppahsyEFdVqQA=="],
|
||||||
|
|
||||||
|
"node-zigar-addon": ["node-zigar-addon@0.15.2", "", { "dependencies": { "node-api-headers": "^1.7.0" } }, "sha512-QjJcPRtUZLkULaFXapAvTzLKKRddgaupr7wQqgDUQo541FMCXAhgWdZJtNcIgCNykJG0bG0Fza5VTKBdSvyavQ=="],
|
||||||
|
|
||||||
"normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
|
"normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
|
||||||
|
|
||||||
"npm-path": ["npm-path@2.0.4", "", { "dependencies": { "which": "^1.2.10" }, "bin": { "npm-path": "bin/npm-path" } }, "sha512-IFsj0R9C7ZdR5cP+ET342q77uSRdtWOlWpih5eC+lu29tIDbNEgDbzgVJ5UFvYHWhxDZ5TFkJafFioO0pPQjCw=="],
|
"npm-path": ["npm-path@2.0.4", "", { "dependencies": { "which": "^1.2.10" }, "bin": { "npm-path": "bin/npm-path" } }, "sha512-IFsj0R9C7ZdR5cP+ET342q77uSRdtWOlWpih5eC+lu29tIDbNEgDbzgVJ5UFvYHWhxDZ5TFkJafFioO0pPQjCw=="],
|
||||||
@ -1053,6 +1064,8 @@
|
|||||||
|
|
||||||
"yoctocolors-cjs": ["yoctocolors-cjs@2.1.3", "", {}, "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw=="],
|
"yoctocolors-cjs": ["yoctocolors-cjs@2.1.3", "", {}, "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw=="],
|
||||||
|
|
||||||
|
"zigar-compiler": ["zigar-compiler@0.15.2", "", {}, "sha512-zlJ8kUwndwrLl4iRlIWEcidC2rcSsfeWM0jvbSoxUVf+SEKd4bVik3z4YDivuyX3SUiUjpCMNyp65etD6BKRmQ=="],
|
||||||
|
|
||||||
"zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
"zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||||
|
|
||||||
"@babel/core/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
"@babel/core/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||||
@ -1097,6 +1110,8 @@
|
|||||||
|
|
||||||
"cmake-js/fs-extra": ["fs-extra@11.3.2", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A=="],
|
"cmake-js/fs-extra": ["fs-extra@11.3.2", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A=="],
|
||||||
|
|
||||||
|
"cmake-js/node-api-headers": ["node-api-headers@1.6.0", "", {}, "sha512-81T99+mWLZnxX0LlZPYuafyFlxVVaWKQ0BDAbSrOqLO+v+gzCzu0GTAVNeVK8lucqjqo9L/1UcK9cpkem8Py4Q=="],
|
||||||
|
|
||||||
"connect/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
"connect/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||||
|
|
||||||
"elliptic/bn.js": ["bn.js@4.12.2", "", {}, "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw=="],
|
"elliptic/bn.js": ["bn.js@4.12.2", "", {}, "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw=="],
|
||||||
|
|||||||
1
dlt-connector/bunfig.toml
Normal file
1
dlt-connector/bunfig.toml
Normal file
@ -0,0 +1 @@
|
|||||||
|
preload = ["bun-zigar"]
|
||||||
@ -7,18 +7,20 @@
|
|||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "bun run src/index.ts",
|
"start": "cross-env TZ=UTC bun run src/index.ts",
|
||||||
"build": "bun build src/index.ts --outdir=build --target=bun --external=gradido-blockchain-js --minify",
|
"build": "bun build src/index.ts --outdir=build --target=bun --external=gradido-blockchain-js --minify",
|
||||||
"dev": "bun run --watch src/index.ts",
|
"dev": "cross-env TZ=UTC bun run --watch src/index.ts",
|
||||||
"migrate": "bun src/migrations/db-v2.7.0_to_blockchain-v3.5",
|
"migrate": "cross-env TZ=UTC bun src/migrations/db-v2.7.0_to_blockchain-v3.7",
|
||||||
"test": "bun test",
|
"test": "cross-env TZ=UTC bun test",
|
||||||
"test:debug": "bun test --inspect-brk",
|
"test:debug": "cross-env TZ=UTC bun test --inspect-brk",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"lint": "biome check --error-on-warnings .",
|
"lint": "biome check --error-on-warnings .",
|
||||||
"lint:fix": "biome check --error-on-warnings . --write"
|
"lint:fix": "biome check --error-on-warnings . --write"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"gradido-blockchain-js": "git+https://github.com/gradido/gradido-blockchain-js#f265dbb1780a912cf8b0418dfe3eaf5cdc5b51cf"
|
"bun-zigar": "^0.15.2",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
|
"gradido-blockchain-js": "git+https://github.com/gradido/gradido-blockchain-js#785fd766289726d41ae01f1e80a274aed871a7fb"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "2.0.0",
|
"@biomejs/biome": "2.0.0",
|
||||||
@ -32,6 +34,7 @@
|
|||||||
"@types/uuid": "^8.3.4",
|
"@types/uuid": "^8.3.4",
|
||||||
"adm-zip": "^0.5.16",
|
"adm-zip": "^0.5.16",
|
||||||
"async-mutex": "^0.5.0",
|
"async-mutex": "^0.5.0",
|
||||||
|
"decimal.js-light": "^2.5.1",
|
||||||
"dotenv": "^10.0.0",
|
"dotenv": "^10.0.0",
|
||||||
"drizzle-orm": "^0.44.7",
|
"drizzle-orm": "^0.44.7",
|
||||||
"elysia": "1.3.8",
|
"elysia": "1.3.8",
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
import { readFileSync } from 'node:fs'
|
import { readFileSync } from 'node:fs'
|
||||||
import { loadCryptoKeys, MemoryBlock } from 'gradido-blockchain-js'
|
import { InMemoryBlockchainProvider, loadCryptoKeys, MemoryBlock } from 'gradido-blockchain-js'
|
||||||
import { configure, getLogger, Logger } from 'log4js'
|
import { configure, getLogger, Logger } from 'log4js'
|
||||||
import * as v from 'valibot'
|
import * as v from 'valibot'
|
||||||
import { CONFIG } from '../config'
|
import { CONFIG } from '../config'
|
||||||
import { MIN_TOPIC_EXPIRE_MILLISECONDS_FOR_UPDATE } from '../config/const'
|
import { MIN_TOPIC_EXPIRE_MILLISECONDS_FOR_UPDATE } from '../config/const'
|
||||||
|
import { KeyPairIdentifierLogic } from '../data/KeyPairIdentifier.logic'
|
||||||
|
import { ResolveKeyPair } from '../interactions/resolveKeyPair/ResolveKeyPair.context'
|
||||||
import { SendToHieroContext } from '../interactions/sendToHiero/SendToHiero.context'
|
import { SendToHieroContext } from '../interactions/sendToHiero/SendToHiero.context'
|
||||||
import { Community, communitySchema } from '../schemas/transaction.schema'
|
import { Community, communitySchema } from '../schemas/transaction.schema'
|
||||||
import { isPortOpenRetry } from '../utils/network'
|
import { isPortOpenRetry } from '../utils/network'
|
||||||
@ -33,11 +35,18 @@ export async function checkHieroAccount(logger: Logger, clients: AppContextClien
|
|||||||
export async function checkHomeCommunity(
|
export async function checkHomeCommunity(
|
||||||
appContext: AppContext,
|
appContext: AppContext,
|
||||||
logger: Logger,
|
logger: Logger,
|
||||||
): Promise<Community> {
|
): Promise<Community | undefined> {
|
||||||
const { backend, hiero } = appContext.clients
|
const { backend, hiero } = appContext.clients
|
||||||
|
|
||||||
// wait for backend server
|
// wait for backend server
|
||||||
await isPortOpenRetry(backend.url)
|
try {
|
||||||
|
logger.info(`Waiting for backend server to become available at ${backend.url}`)
|
||||||
|
await isPortOpenRetry(backend.url)
|
||||||
|
} catch (e) {
|
||||||
|
logger.error(`Backend server at ${backend.url} is not reachable (${e})`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// ask backend for home community
|
// ask backend for home community
|
||||||
let homeCommunity = await backend.getHomeCommunityDraft()
|
let homeCommunity = await backend.getHomeCommunityDraft()
|
||||||
// on missing topicId, create one
|
// on missing topicId, create one
|
||||||
@ -56,7 +65,7 @@ export async function checkHomeCommunity(
|
|||||||
await hiero.updateTopic(homeCommunity.hieroTopicId)
|
await hiero.updateTopic(homeCommunity.hieroTopicId)
|
||||||
topicInfo = await hiero.getTopicInfo(homeCommunity.hieroTopicId)
|
topicInfo = await hiero.getTopicInfo(homeCommunity.hieroTopicId)
|
||||||
logger.info(
|
logger.info(
|
||||||
`updated topic info, new expiration time: ${topicInfo.expirationTime.toLocaleDateString()}`,
|
`Topic expiration extended. New expiration time: ${topicInfo.expirationTime.toLocaleDateString()}`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -64,9 +73,16 @@ export async function checkHomeCommunity(
|
|||||||
throw new Error('still no topic id, after creating topic and update community in backend.')
|
throw new Error('still no topic id, after creating topic and update community in backend.')
|
||||||
}
|
}
|
||||||
appContext.cache.setHomeCommunityTopicId(homeCommunity.hieroTopicId)
|
appContext.cache.setHomeCommunityTopicId(homeCommunity.hieroTopicId)
|
||||||
logger.info(`home community topic: ${homeCommunity.hieroTopicId}`)
|
logger.info(`Home community topic id: ${homeCommunity.hieroTopicId}`)
|
||||||
logger.info(`gradido node server: ${appContext.clients.gradidoNode.url}`)
|
logger.info(`Gradido node server: ${appContext.clients.gradidoNode.url}`)
|
||||||
logger.info(`gradido backend server: ${appContext.clients.backend.url}`)
|
logger.info(`Gradido backend server: ${appContext.clients.backend.url}`)
|
||||||
|
|
||||||
|
await ResolveKeyPair(
|
||||||
|
new KeyPairIdentifierLogic({
|
||||||
|
communityTopicId: homeCommunity.hieroTopicId,
|
||||||
|
communityId: homeCommunity.uuid,
|
||||||
|
}),
|
||||||
|
)
|
||||||
return v.parse(communitySchema, homeCommunity)
|
return v.parse(communitySchema, homeCommunity)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -80,10 +96,11 @@ export async function checkGradidoNode(
|
|||||||
|
|
||||||
// ask gradido node if community blockchain was created
|
// ask gradido node if community blockchain was created
|
||||||
try {
|
try {
|
||||||
|
InMemoryBlockchainProvider.getInstance().getBlockchain(homeCommunity.uuid)
|
||||||
if (
|
if (
|
||||||
!(await clients.gradidoNode.getTransaction({
|
!(await clients.gradidoNode.getTransaction({
|
||||||
transactionId: 1,
|
transactionId: 1,
|
||||||
topic: homeCommunity.hieroTopicId,
|
communityId: homeCommunity.uuid,
|
||||||
}))
|
}))
|
||||||
) {
|
) {
|
||||||
// if not exist, create community root transaction
|
// if not exist, create community root transaction
|
||||||
|
|||||||
@ -7,11 +7,7 @@ import { exportCommunities } from '../client/GradidoNode/communities'
|
|||||||
import { GradidoNodeProcess } from '../client/GradidoNode/GradidoNodeProcess'
|
import { GradidoNodeProcess } from '../client/GradidoNode/GradidoNodeProcess'
|
||||||
import { HieroClient } from '../client/hiero/HieroClient'
|
import { HieroClient } from '../client/hiero/HieroClient'
|
||||||
import { CONFIG } from '../config'
|
import { CONFIG } from '../config'
|
||||||
import {
|
import { GRADIDO_NODE_HOME_FOLDER_NAME, LOG4JS_BASE_CATEGORY } from '../config/const'
|
||||||
GRADIDO_NODE_HOME_FOLDER_NAME,
|
|
||||||
GRADIDO_NODE_RUNTIME_PATH,
|
|
||||||
LOG4JS_BASE_CATEGORY,
|
|
||||||
} from '../config/const'
|
|
||||||
import { checkFileExist, checkPathExist } from '../utils/filesystem'
|
import { checkFileExist, checkPathExist } from '../utils/filesystem'
|
||||||
import { isPortOpen } from '../utils/network'
|
import { isPortOpen } from '../utils/network'
|
||||||
import { AppContextClients } from './appContext'
|
import { AppContextClients } from './appContext'
|
||||||
@ -37,7 +33,7 @@ export async function initGradidoNode(clients: AppContextClients): Promise<void>
|
|||||||
// write Hedera Address Book
|
// write Hedera Address Book
|
||||||
exportHederaAddressbooks(gradidoNodeHomeFolder, clients.hiero),
|
exportHederaAddressbooks(gradidoNodeHomeFolder, clients.hiero),
|
||||||
// check GradidoNode Runtime, download when missing
|
// check GradidoNode Runtime, download when missing
|
||||||
ensureGradidoNodeRuntimeAvailable(GRADIDO_NODE_RUNTIME_PATH),
|
ensureGradidoNodeRuntimeAvailable(GradidoNodeProcess.getRuntimePathFileName()),
|
||||||
// export communities to GradidoNode Folder
|
// export communities to GradidoNode Folder
|
||||||
exportCommunities(gradidoNodeHomeFolder, clients.backend),
|
exportCommunities(gradidoNodeHomeFolder, clients.backend),
|
||||||
])
|
])
|
||||||
@ -57,11 +53,22 @@ async function exportHederaAddressbooks(
|
|||||||
|
|
||||||
async function ensureGradidoNodeRuntimeAvailable(runtimeFileName: string): Promise<void> {
|
async function ensureGradidoNodeRuntimeAvailable(runtimeFileName: string): Promise<void> {
|
||||||
const runtimeFolder = path.dirname(runtimeFileName)
|
const runtimeFolder = path.dirname(runtimeFileName)
|
||||||
|
const wantedVersion = `v${CONFIG.DLT_GRADIDO_NODE_SERVER_VERSION}`
|
||||||
checkPathExist(runtimeFolder, true)
|
checkPathExist(runtimeFolder, true)
|
||||||
if (!checkFileExist(runtimeFileName)) {
|
let versionMatch = false
|
||||||
|
const isFileExist = checkFileExist(runtimeFileName)
|
||||||
|
if (isFileExist) {
|
||||||
|
const foundVersion = await GradidoNodeProcess.checkRuntimeVersion()
|
||||||
|
if (wantedVersion !== foundVersion) {
|
||||||
|
logger.info(`GradidoNode version detected: ${foundVersion}, required: ${wantedVersion}`)
|
||||||
|
} else {
|
||||||
|
versionMatch = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!isFileExist || !versionMatch) {
|
||||||
const runtimeArchiveFilename = createGradidoNodeRuntimeArchiveFilename()
|
const runtimeArchiveFilename = createGradidoNodeRuntimeArchiveFilename()
|
||||||
const downloadUrl = new URL(
|
const downloadUrl = new URL(
|
||||||
`https://github.com/gradido/gradido_node/releases/download/v${CONFIG.DLT_GRADIDO_NODE_SERVER_VERSION}/${runtimeArchiveFilename}`,
|
`https://github.com/gradido/gradido_node/releases/download/${wantedVersion}/${runtimeArchiveFilename}`,
|
||||||
)
|
)
|
||||||
logger.debug(`download GradidoNode Runtime from ${downloadUrl}`)
|
logger.debug(`download GradidoNode Runtime from ${downloadUrl}`)
|
||||||
const archive = await fetch(downloadUrl)
|
const archive = await fetch(downloadUrl)
|
||||||
|
|||||||
10
dlt-connector/src/cache/KeyPairCacheManager.ts
vendored
10
dlt-connector/src/cache/KeyPairCacheManager.ts
vendored
@ -76,4 +76,14 @@ export class KeyPairCacheManager {
|
|||||||
}
|
}
|
||||||
return keyPair
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import { LOG4JS_BASE_CATEGORY } from '../../config/const'
|
|||||||
import { AddressType } from '../../data/AddressType.enum'
|
import { AddressType } from '../../data/AddressType.enum'
|
||||||
import { Uuidv4Hash } from '../../data/Uuidv4Hash'
|
import { Uuidv4Hash } from '../../data/Uuidv4Hash'
|
||||||
import { addressTypeSchema, confirmedTransactionSchema } from '../../schemas/typeConverter.schema'
|
import { addressTypeSchema, confirmedTransactionSchema } from '../../schemas/typeConverter.schema'
|
||||||
import { Hex32, Hex32Input, HieroId, hex32Schema } from '../../schemas/typeGuard.schema'
|
import { Hex32, Hex32Input, hex32Schema, Uuidv4 } from '../../schemas/typeGuard.schema'
|
||||||
import { isPortOpenRetry } from '../../utils/network'
|
import { isPortOpenRetry } from '../../utils/network'
|
||||||
import { GradidoNodeErrorCodes } from './GradidoNodeErrorCodes'
|
import { GradidoNodeErrorCodes } from './GradidoNodeErrorCodes'
|
||||||
import {
|
import {
|
||||||
@ -75,7 +75,10 @@ export class GradidoNodeClient {
|
|||||||
const response = await this.rpcCall<{ transaction: string }>('getTransaction', parameter)
|
const response = await this.rpcCall<{ transaction: string }>('getTransaction', parameter)
|
||||||
if (response.isSuccess()) {
|
if (response.isSuccess()) {
|
||||||
// this.logger.debug('result: ', response.result.transaction)
|
// this.logger.debug('result: ', response.result.transaction)
|
||||||
return v.parse(confirmedTransactionSchema, response.result.transaction)
|
return v.parse(confirmedTransactionSchema, {
|
||||||
|
base64: response.result.transaction,
|
||||||
|
communityId: parameter.communityId,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
if (response.isError()) {
|
if (response.isError()) {
|
||||||
if (response.error.code === GradidoNodeErrorCodes.TRANSACTION_NOT_FOUND) {
|
if (response.error.code === GradidoNodeErrorCodes.TRANSACTION_NOT_FOUND) {
|
||||||
@ -88,19 +91,22 @@ export class GradidoNodeClient {
|
|||||||
/**
|
/**
|
||||||
* getLastTransaction
|
* getLastTransaction
|
||||||
* get the last confirmed transaction from a specific community
|
* get the last confirmed transaction from a specific community
|
||||||
* @param hieroTopic the community hiero topic id
|
* @param communityId the community id
|
||||||
* @returns the last confirmed transaction or undefined if blockchain for community is empty or not found
|
* @returns the last confirmed transaction or undefined if blockchain for community is empty or not found
|
||||||
* @throws GradidoNodeRequestError
|
* @throws GradidoNodeRequestError
|
||||||
*/
|
*/
|
||||||
|
|
||||||
public async getLastTransaction(hieroTopic: HieroId): Promise<ConfirmedTransaction | undefined> {
|
public async getLastTransaction(communityId: Uuidv4): Promise<ConfirmedTransaction | undefined> {
|
||||||
const parameter = {
|
const parameter = {
|
||||||
format: 'base64',
|
format: 'base64',
|
||||||
topic: hieroTopic,
|
communityId,
|
||||||
}
|
}
|
||||||
const response = await this.rpcCall<{ transaction: string }>('getLastTransaction', parameter)
|
const response = await this.rpcCall<{ transaction: string }>('getLastTransaction', parameter)
|
||||||
if (response.isSuccess()) {
|
if (response.isSuccess()) {
|
||||||
return v.parse(confirmedTransactionSchema, response.result.transaction)
|
return v.parse(confirmedTransactionSchema, {
|
||||||
|
base64: response.result.transaction,
|
||||||
|
communityId: parameter.communityId,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
if (response.isError()) {
|
if (response.isError()) {
|
||||||
if (response.error.code === GradidoNodeErrorCodes.GRADIDO_NODE_ERROR) {
|
if (response.error.code === GradidoNodeErrorCodes.GRADIDO_NODE_ERROR) {
|
||||||
@ -115,7 +121,7 @@ export class GradidoNodeClient {
|
|||||||
* get list of confirmed transactions from a specific community
|
* get list of confirmed transactions from a specific community
|
||||||
* @param input fromTransactionId is the id of the first transaction to return
|
* @param input fromTransactionId is the id of the first transaction to return
|
||||||
* @param input maxResultCount is the max number of transactions to return
|
* @param input maxResultCount is the max number of transactions to return
|
||||||
* @param input topic is the community hiero topic id
|
* @param input communityId is the community id
|
||||||
* @returns list of confirmed transactions
|
* @returns list of confirmed transactions
|
||||||
* @throws GradidoNodeRequestError
|
* @throws GradidoNodeRequestError
|
||||||
* @example
|
* @example
|
||||||
@ -123,7 +129,7 @@ export class GradidoNodeClient {
|
|||||||
* const transactions = await getTransactions({
|
* const transactions = await getTransactions({
|
||||||
* fromTransactionId: 1,
|
* fromTransactionId: 1,
|
||||||
* maxResultCount: 100,
|
* maxResultCount: 100,
|
||||||
* topic: communityUuid,
|
* communityId: communityUuid,
|
||||||
* })
|
* })
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
@ -137,7 +143,10 @@ export class GradidoNodeClient {
|
|||||||
parameter,
|
parameter,
|
||||||
)
|
)
|
||||||
return result.transactions.map((transactionBase64) =>
|
return result.transactions.map((transactionBase64) =>
|
||||||
v.parse(confirmedTransactionSchema, transactionBase64),
|
v.parse(confirmedTransactionSchema, {
|
||||||
|
base64: transactionBase64,
|
||||||
|
communityId: parameter.communityId,
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -163,7 +172,10 @@ export class GradidoNodeClient {
|
|||||||
parameter,
|
parameter,
|
||||||
)
|
)
|
||||||
return response.transactions.map((transactionBase64) =>
|
return response.transactions.map((transactionBase64) =>
|
||||||
v.parse(confirmedTransactionSchema, transactionBase64),
|
v.parse(confirmedTransactionSchema, {
|
||||||
|
base64: transactionBase64,
|
||||||
|
communityId: parameter.communityId,
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -173,15 +185,15 @@ export class GradidoNodeClient {
|
|||||||
* can be used to check if user/account exists on blockchain
|
* can be used to check if user/account exists on blockchain
|
||||||
* look also for gmw, auf and deferred transfer accounts
|
* look also for gmw, auf and deferred transfer accounts
|
||||||
* @param pubkey the public key of the user or account
|
* @param pubkey the public key of the user or account
|
||||||
* @param hieroTopic the community hiero topic id
|
* @param communityId the community id
|
||||||
* @returns the address type of the user/account, AddressType.NONE if not found
|
* @returns the address type of the user/account, AddressType.NONE if not found
|
||||||
* @throws GradidoNodeRequestError
|
* @throws GradidoNodeRequestError
|
||||||
*/
|
*/
|
||||||
|
|
||||||
public async getAddressType(pubkey: Hex32Input, hieroTopic: HieroId): Promise<AddressType> {
|
public async getAddressType(pubkey: Hex32Input, communityId: Uuidv4): Promise<AddressType> {
|
||||||
const parameter = {
|
const parameter = {
|
||||||
pubkey: v.parse(hex32Schema, pubkey),
|
pubkey: v.parse(hex32Schema, pubkey),
|
||||||
topic: hieroTopic,
|
communityId,
|
||||||
}
|
}
|
||||||
const response = await this.rpcCallResolved<{ addressType: string }>(
|
const response = await this.rpcCallResolved<{ addressType: string }>(
|
||||||
'getAddressType',
|
'getAddressType',
|
||||||
@ -194,17 +206,17 @@ export class GradidoNodeClient {
|
|||||||
* findUserByNameHash
|
* findUserByNameHash
|
||||||
* find a user by name hash
|
* find a user by name hash
|
||||||
* @param nameHash the name hash of the user
|
* @param nameHash the name hash of the user
|
||||||
* @param hieroTopic the community hiero topic id
|
* @param communityId the community id
|
||||||
* @returns the public key of the user as hex32 string or undefined if user is not found
|
* @returns the public key of the user as hex32 string or undefined if user is not found
|
||||||
* @throws GradidoNodeRequestError
|
* @throws GradidoNodeRequestError
|
||||||
*/
|
*/
|
||||||
public async findUserByNameHash(
|
public async findUserByNameHash(
|
||||||
nameHash: Uuidv4Hash,
|
nameHash: Uuidv4Hash,
|
||||||
hieroTopic: HieroId,
|
communityId: Uuidv4,
|
||||||
): Promise<Hex32 | undefined> {
|
): Promise<Hex32 | undefined> {
|
||||||
const parameter = {
|
const parameter = {
|
||||||
nameHash: nameHash.getAsHexString(),
|
nameHash: nameHash.getAsHexString(),
|
||||||
topic: hieroTopic,
|
communityId,
|
||||||
}
|
}
|
||||||
const response = await this.rpcCall<{ pubkey: string; timeUsed: string }>(
|
const response = await this.rpcCall<{ pubkey: string; timeUsed: string }>(
|
||||||
'findUserByNameHash',
|
'findUserByNameHash',
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
|
import path from 'node:path'
|
||||||
import { Mutex } from 'async-mutex'
|
import { Mutex } from 'async-mutex'
|
||||||
import { Subprocess, spawn } from 'bun'
|
import { $, Subprocess, spawn } from 'bun'
|
||||||
import { getLogger, Logger } from 'log4js'
|
import { getLogger, Logger } from 'log4js'
|
||||||
import { CONFIG } from '../../config'
|
import { CONFIG } from '../../config'
|
||||||
import {
|
import {
|
||||||
GRADIDO_NODE_KILL_TIMEOUT_MILLISECONDS,
|
GRADIDO_NODE_KILL_TIMEOUT_MILLISECONDS,
|
||||||
GRADIDO_NODE_MIN_RUNTIME_BEFORE_EXIT_MILLISECONDS,
|
GRADIDO_NODE_MIN_RUNTIME_BEFORE_EXIT_MILLISECONDS,
|
||||||
GRADIDO_NODE_MIN_RUNTIME_BEFORE_RESTART_MILLISECONDS,
|
GRADIDO_NODE_MIN_RUNTIME_BEFORE_RESTART_MILLISECONDS,
|
||||||
GRADIDO_NODE_RUNTIME_PATH,
|
|
||||||
LOG4JS_BASE_CATEGORY,
|
LOG4JS_BASE_CATEGORY,
|
||||||
} from '../../config/const'
|
} from '../../config/const'
|
||||||
import { delay } from '../../utils/time'
|
import { delay } from '../../utils/time'
|
||||||
@ -43,20 +43,33 @@ export class GradidoNodeProcess {
|
|||||||
return GradidoNodeProcess.instance
|
return GradidoNodeProcess.instance
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static getRuntimePathFileName(): string {
|
||||||
|
const isWindows = process.platform === 'win32'
|
||||||
|
const binaryName = isWindows ? 'GradidoNode.exe' : 'GradidoNode'
|
||||||
|
|
||||||
|
return path.join(CONFIG.DLT_GRADIDO_NODE_SERVER_HOME_FOLDER, 'bin', binaryName)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async checkRuntimeVersion(): Promise<string> {
|
||||||
|
return (await $`${GradidoNodeProcess.getRuntimePathFileName()} --version`.text()).trim()
|
||||||
|
}
|
||||||
|
|
||||||
public start() {
|
public start() {
|
||||||
if (this.proc) {
|
if (this.proc) {
|
||||||
this.logger.warn('GradidoNodeProcess already running.')
|
this.logger.warn('GradidoNodeProcess already running.')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.logger.info(`starting GradidoNodeProcess with path: ${GRADIDO_NODE_RUNTIME_PATH}`)
|
const gradidoNodeRuntimePath = GradidoNodeProcess.getRuntimePathFileName()
|
||||||
|
this.logger.info(`starting GradidoNodeProcess with path: ${gradidoNodeRuntimePath}`)
|
||||||
this.lastStarted = new Date()
|
this.lastStarted = new Date()
|
||||||
const logger = this.logger
|
const logger = this.logger
|
||||||
this.proc = spawn([GRADIDO_NODE_RUNTIME_PATH], {
|
this.proc = spawn([gradidoNodeRuntimePath], {
|
||||||
env: {
|
env: {
|
||||||
CLIENTS_HIERO_NETWORKTYPE: CONFIG.HIERO_HEDERA_NETWORK,
|
CLIENTS_HIERO_NETWORKTYPE: CONFIG.HIERO_HEDERA_NETWORK,
|
||||||
SERVER_JSON_RPC_PORT: CONFIG.DLT_NODE_SERVER_PORT.toString(),
|
SERVER_JSON_RPC_PORT: CONFIG.DLT_NODE_SERVER_PORT.toString(),
|
||||||
USERPROFILE: CONFIG.DLT_GRADIDO_NODE_SERVER_HOME_FOLDER,
|
USERPROFILE: CONFIG.DLT_GRADIDO_NODE_SERVER_HOME_FOLDER,
|
||||||
HOME: CONFIG.DLT_GRADIDO_NODE_SERVER_HOME_FOLDER,
|
HOME: CONFIG.DLT_GRADIDO_NODE_SERVER_HOME_FOLDER,
|
||||||
|
UNSECURE_ALLOW_CORS_ALL: CONFIG.DLT_GRADIDO_NODE_SERVER_ALLOW_CORS ? '1' : '0',
|
||||||
},
|
},
|
||||||
onExit(_proc, exitCode, signalCode, error) {
|
onExit(_proc, exitCode, signalCode, error) {
|
||||||
logger.warn(`GradidoNodeProcess exited with code ${exitCode} and signalCode ${signalCode}`)
|
logger.warn(`GradidoNodeProcess exited with code ${exitCode} and signalCode ${signalCode}`)
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import { getLogger } from 'log4js'
|
|||||||
import { CONFIG } from '../../config'
|
import { CONFIG } from '../../config'
|
||||||
import { GRADIDO_NODE_HOME_FOLDER_NAME, LOG4JS_BASE_CATEGORY } from '../../config/const'
|
import { GRADIDO_NODE_HOME_FOLDER_NAME, LOG4JS_BASE_CATEGORY } from '../../config/const'
|
||||||
import { HieroId } from '../../schemas/typeGuard.schema'
|
import { HieroId } from '../../schemas/typeGuard.schema'
|
||||||
import { checkFileExist, checkPathExist } from '../../utils/filesystem'
|
import { checkFileExist, checkPathExist, toFolderName } from '../../utils/filesystem'
|
||||||
import { BackendClient } from '../backend/BackendClient'
|
import { BackendClient } from '../backend/BackendClient'
|
||||||
import { GradidoNodeProcess } from './GradidoNodeProcess'
|
import { GradidoNodeProcess } from './GradidoNodeProcess'
|
||||||
|
|
||||||
@ -15,7 +15,7 @@ const ensureCommunitiesAvailableMutex: Mutex = new Mutex()
|
|||||||
// prototype, later add api call to gradido dlt node server for adding/updating communities
|
// prototype, later add api call to gradido dlt node server for adding/updating communities
|
||||||
type CommunityForDltNodeServer = {
|
type CommunityForDltNodeServer = {
|
||||||
communityId: string
|
communityId: string
|
||||||
hieroTopicId: string
|
hieroTopicId?: string | null
|
||||||
alias: string
|
alias: string
|
||||||
folder: string
|
folder: string
|
||||||
}
|
}
|
||||||
@ -38,28 +38,20 @@ export async function ensureCommunitiesAvailable(communityTopicIds: HieroId[]):
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function exportCommunities(homeFolder: string, client: BackendClient): Promise<void> {
|
export async function exportCommunities(homeFolder: string, client: BackendClient): Promise<void> {
|
||||||
const communities = await client.getReachableCommunities()
|
const communities = await client.getAuthorizedCommunities()
|
||||||
const communitiesPath = path.join(homeFolder, 'communities.json')
|
const communitiesPath = path.join(homeFolder, 'communities.json')
|
||||||
checkPathExist(path.dirname(communitiesPath), true)
|
checkPathExist(path.dirname(communitiesPath), true)
|
||||||
// make sure communityName is unique
|
|
||||||
const communityName = new Set<string>()
|
|
||||||
const communitiesForDltNodeServer: CommunityForDltNodeServer[] = []
|
const communitiesForDltNodeServer: CommunityForDltNodeServer[] = []
|
||||||
for (const com of communities) {
|
for (const com of communities) {
|
||||||
if (!com.uuid || !com.hieroTopicId) {
|
if (!com.uuid) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// use name as alias if not empty and unique, otherwise use uuid
|
|
||||||
let alias = com.name
|
|
||||||
if (!alias || communityName.has(alias)) {
|
|
||||||
alias = com.uuid
|
|
||||||
}
|
|
||||||
communityName.add(alias)
|
|
||||||
communitiesForDltNodeServer.push({
|
communitiesForDltNodeServer.push({
|
||||||
communityId: com.uuid,
|
communityId: com.uuid,
|
||||||
hieroTopicId: com.hieroTopicId,
|
hieroTopicId: com.hieroTopicId,
|
||||||
alias,
|
alias: com.name,
|
||||||
// use only alpha-numeric chars for folder name
|
// use only alpha-numeric chars for folder name
|
||||||
folder: alias.replace(/[^a-zA-Z0-9]/g, '_'),
|
folder: toFolderName(com.uuid),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
fs.writeFileSync(communitiesPath, JSON.stringify(communitiesForDltNodeServer, null, 2))
|
fs.writeFileSync(communitiesPath, JSON.stringify(communitiesForDltNodeServer, null, 2))
|
||||||
|
|||||||
@ -1,18 +1,20 @@
|
|||||||
import { beforeAll, describe, expect, it } from 'bun:test'
|
import { beforeAll, describe, expect, it } from 'bun:test'
|
||||||
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
import * as v from 'valibot'
|
import * as v from 'valibot'
|
||||||
import {
|
import {
|
||||||
HieroId,
|
|
||||||
HieroTransactionIdString,
|
HieroTransactionIdString,
|
||||||
hieroIdSchema,
|
hieroIdSchema,
|
||||||
hieroTransactionIdStringSchema,
|
hieroTransactionIdStringSchema,
|
||||||
|
Uuidv4,
|
||||||
|
uuidv4Schema,
|
||||||
} from '../../schemas/typeGuard.schema'
|
} from '../../schemas/typeGuard.schema'
|
||||||
import { transactionIdentifierSchema } from './input.schema'
|
import { transactionIdentifierSchema } from './input.schema'
|
||||||
|
|
||||||
let topic: HieroId
|
let communityId: Uuidv4
|
||||||
const topicString = '0.0.261'
|
const uuidv4String = uuidv4()
|
||||||
let hieroTransactionId: HieroTransactionIdString
|
let hieroTransactionId: HieroTransactionIdString
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
topic = v.parse(hieroIdSchema, topicString)
|
communityId = v.parse(uuidv4Schema, uuidv4String)
|
||||||
hieroTransactionId = v.parse(hieroTransactionIdStringSchema, '0.0.261-1755348116-1281621')
|
hieroTransactionId = v.parse(hieroTransactionIdStringSchema, '0.0.261-1755348116-1281621')
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -21,39 +23,39 @@ describe('transactionIdentifierSchema ', () => {
|
|||||||
expect(
|
expect(
|
||||||
v.parse(transactionIdentifierSchema, {
|
v.parse(transactionIdentifierSchema, {
|
||||||
transactionId: 1,
|
transactionId: 1,
|
||||||
topic: topicString,
|
communityId,
|
||||||
}),
|
}),
|
||||||
).toEqual({
|
).toEqual({
|
||||||
transactionId: 1,
|
transactionId: 1,
|
||||||
hieroTransactionId: undefined,
|
hieroTransactionId: undefined,
|
||||||
topic,
|
communityId,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
it('valid, transaction identified by hieroTransactionId and topic', () => {
|
it('valid, transaction identified by hieroTransactionId and topic', () => {
|
||||||
expect(
|
expect(
|
||||||
v.parse(transactionIdentifierSchema, {
|
v.parse(transactionIdentifierSchema, {
|
||||||
hieroTransactionId: '0.0.261-1755348116-1281621',
|
hieroTransactionId: '0.0.261-1755348116-1281621',
|
||||||
topic: topicString,
|
communityId,
|
||||||
}),
|
}),
|
||||||
).toEqual({
|
).toEqual({
|
||||||
hieroTransactionId,
|
hieroTransactionId,
|
||||||
topic,
|
communityId,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
it('invalid, missing topic', () => {
|
it('invalid, missing communityId', () => {
|
||||||
expect(() =>
|
expect(() =>
|
||||||
v.parse(transactionIdentifierSchema, {
|
v.parse(transactionIdentifierSchema, {
|
||||||
transactionId: 1,
|
transactionId: 1,
|
||||||
hieroTransactionId: '0.0.261-1755348116-1281621',
|
hieroTransactionId: '0.0.261-1755348116-1281621',
|
||||||
}),
|
}),
|
||||||
).toThrowError(new Error('Invalid key: Expected "topic" but received undefined'))
|
).toThrowError(new Error('Invalid key: Expected "communityId" but received undefined'))
|
||||||
})
|
})
|
||||||
it('invalid, transactionNr and iotaMessageId set', () => {
|
it('invalid, transactionNr and iotaMessageId set', () => {
|
||||||
expect(() =>
|
expect(() =>
|
||||||
v.parse(transactionIdentifierSchema, {
|
v.parse(transactionIdentifierSchema, {
|
||||||
transactionId: 1,
|
transactionId: 1,
|
||||||
hieroTransactionId: '0.0.261-1755348116-1281621',
|
hieroTransactionId: '0.0.261-1755348116-1281621',
|
||||||
topic,
|
communityId,
|
||||||
}),
|
}),
|
||||||
).toThrowError(new Error('expect transactionNr or hieroTransactionId not both'))
|
).toThrowError(new Error('expect transactionNr or hieroTransactionId not both'))
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
import * as v from 'valibot'
|
import * as v from 'valibot'
|
||||||
import { hieroIdSchema, hieroTransactionIdStringSchema } from '../../schemas/typeGuard.schema'
|
import { hieroTransactionIdStringSchema, uuidv4Schema } from '../../schemas/typeGuard.schema'
|
||||||
|
|
||||||
export const transactionsRangeSchema = v.object({
|
export const transactionsRangeSchema = v.object({
|
||||||
// default value is 1, from first transactions
|
// default value is 1, from first transactions
|
||||||
fromTransactionId: v.nullish(v.pipe(v.number(), v.minValue(1, 'expect number >= 1')), 1),
|
fromTransactionId: v.nullish(v.pipe(v.number(), v.minValue(1, 'expect number >= 1')), 1),
|
||||||
// default value is 100, max 100 transactions
|
// default value is 100, max 100 transactions
|
||||||
maxResultCount: v.nullish(v.pipe(v.number(), v.minValue(1, 'expect number >= 1')), 100),
|
maxResultCount: v.nullish(v.pipe(v.number(), v.minValue(1, 'expect number >= 1')), 100),
|
||||||
topic: hieroIdSchema,
|
communityId: uuidv4Schema,
|
||||||
})
|
})
|
||||||
|
|
||||||
export type TransactionsRangeInput = v.InferInput<typeof transactionsRangeSchema>
|
export type TransactionsRangeInput = v.InferInput<typeof transactionsRangeSchema>
|
||||||
@ -19,7 +19,7 @@ export const transactionIdentifierSchema = v.pipe(
|
|||||||
undefined,
|
undefined,
|
||||||
),
|
),
|
||||||
hieroTransactionId: v.nullish(hieroTransactionIdStringSchema, undefined),
|
hieroTransactionId: v.nullish(hieroTransactionIdStringSchema, undefined),
|
||||||
topic: hieroIdSchema,
|
communityId: uuidv4Schema,
|
||||||
}),
|
}),
|
||||||
v.custom((value: any) => {
|
v.custom((value: any) => {
|
||||||
const setFieldsCount =
|
const setFieldsCount =
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { CONFIG } from '../../config'
|
|||||||
import { LOG4JS_BASE_CATEGORY } from '../../config/const'
|
import { LOG4JS_BASE_CATEGORY } from '../../config/const'
|
||||||
import { HieroId, Uuidv4 } from '../../schemas/typeGuard.schema'
|
import { HieroId, Uuidv4 } from '../../schemas/typeGuard.schema'
|
||||||
import {
|
import {
|
||||||
|
getAuthorizedCommunities,
|
||||||
getReachableCommunities,
|
getReachableCommunities,
|
||||||
homeCommunityGraphqlQuery,
|
homeCommunityGraphqlQuery,
|
||||||
setHomeCommunityTopicId,
|
setHomeCommunityTopicId,
|
||||||
@ -101,6 +102,19 @@ export class BackendClient {
|
|||||||
return v.parse(v.array(communitySchema), data.reachableCommunities)
|
return v.parse(v.array(communitySchema), data.reachableCommunities)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getAuthorizedCommunities(): Promise<Community[]> {
|
||||||
|
this.logger.info('get authorized communities on backend')
|
||||||
|
const { data, errors } = await this.client.rawRequest<{ authorizedCommunities: Community[] }>(
|
||||||
|
getAuthorizedCommunities,
|
||||||
|
{},
|
||||||
|
await this.getRequestHeader(),
|
||||||
|
)
|
||||||
|
if (errors) {
|
||||||
|
throw errors[0]
|
||||||
|
}
|
||||||
|
return v.parse(v.array(communitySchema), data.authorizedCommunities)
|
||||||
|
}
|
||||||
|
|
||||||
private async getRequestHeader(): Promise<{
|
private async getRequestHeader(): Promise<{
|
||||||
authorization: string
|
authorization: string
|
||||||
}> {
|
}> {
|
||||||
|
|||||||
@ -44,3 +44,12 @@ export const getReachableCommunities = gql`
|
|||||||
}
|
}
|
||||||
${communityFragment}
|
${communityFragment}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
export const getAuthorizedCommunities = gql`
|
||||||
|
query {
|
||||||
|
authorizedCommunities {
|
||||||
|
...Community_common
|
||||||
|
}
|
||||||
|
}
|
||||||
|
${communityFragment}
|
||||||
|
`
|
||||||
|
|||||||
@ -20,7 +20,7 @@ import { getLogger, Logger } from 'log4js'
|
|||||||
import * as v from 'valibot'
|
import * as v from 'valibot'
|
||||||
import { CONFIG } from '../../config'
|
import { CONFIG } from '../../config'
|
||||||
import { LOG4JS_BASE_CATEGORY } from '../../config/const'
|
import { LOG4JS_BASE_CATEGORY } from '../../config/const'
|
||||||
import { HieroId, hieroIdSchema } from '../../schemas/typeGuard.schema'
|
import { HieroId, hieroIdSchema, Uuidv4 } from '../../schemas/typeGuard.schema'
|
||||||
import { durationInMinutesFromDates, printTimeDuration } from '../../utils/time'
|
import { durationInMinutesFromDates, printTimeDuration } from '../../utils/time'
|
||||||
import { GradidoNodeClient } from '../GradidoNode/GradidoNodeClient'
|
import { GradidoNodeClient } from '../GradidoNode/GradidoNodeClient'
|
||||||
import { GradidoNodeProcess } from '../GradidoNode/GradidoNodeProcess'
|
import { GradidoNodeProcess } from '../GradidoNode/GradidoNodeProcess'
|
||||||
@ -72,6 +72,7 @@ export class HieroClient {
|
|||||||
|
|
||||||
public async sendMessage(
|
public async sendMessage(
|
||||||
topicId: HieroId,
|
topicId: HieroId,
|
||||||
|
communityId: Uuidv4,
|
||||||
transaction: GradidoTransaction,
|
transaction: GradidoTransaction,
|
||||||
): Promise<TransactionId | null> {
|
): Promise<TransactionId | null> {
|
||||||
const timeUsed = new Profiler()
|
const timeUsed = new Profiler()
|
||||||
@ -99,10 +100,10 @@ export class HieroClient {
|
|||||||
)
|
)
|
||||||
// TODO: fix issue in GradidoNode
|
// TODO: fix issue in GradidoNode
|
||||||
// hot fix, when gradido node is running some time, the hiero listener stop working, so we check if our new transaction is received
|
// hot fix, when gradido node is running some time, the hiero listener stop working, so we check if our new transaction is received
|
||||||
// after 10 seconds, else restart GradidoNode
|
// after 20 seconds, else restart GradidoNode
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
const transaction = await GradidoNodeClient.getInstance().getTransaction({
|
const transaction = await GradidoNodeClient.getInstance().getTransaction({
|
||||||
topic: topicId,
|
communityId,
|
||||||
hieroTransactionId: sendResponse.transactionId.toString(),
|
hieroTransactionId: sendResponse.transactionId.toString(),
|
||||||
})
|
})
|
||||||
if (!transaction) {
|
if (!transaction) {
|
||||||
@ -121,7 +122,7 @@ export class HieroClient {
|
|||||||
GradidoNodeProcess.getInstance().start()
|
GradidoNodeProcess.getInstance().start()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, 10000)
|
}, 20000)
|
||||||
if (logger.isInfoEnabled()) {
|
if (logger.isInfoEnabled()) {
|
||||||
// only for logging
|
// only for logging
|
||||||
sendResponse.getReceiptWithSigner(this.wallet).then((receipt) => {
|
sendResponse.getReceiptWithSigner(this.wallet).then((receipt) => {
|
||||||
|
|||||||
@ -17,6 +17,6 @@ export const GRADIDO_NODE_RUNTIME_PATH = path.join(
|
|||||||
// if last start was less than this time, do not restart
|
// if last start was less than this time, do not restart
|
||||||
export const GRADIDO_NODE_MIN_RUNTIME_BEFORE_RESTART_MILLISECONDS = 1000 * 30
|
export const GRADIDO_NODE_MIN_RUNTIME_BEFORE_RESTART_MILLISECONDS = 1000 * 30
|
||||||
export const GRADIDO_NODE_MIN_RUNTIME_BEFORE_EXIT_MILLISECONDS = 1000 * 2
|
export const GRADIDO_NODE_MIN_RUNTIME_BEFORE_EXIT_MILLISECONDS = 1000 * 2
|
||||||
export const GRADIDO_NODE_KILL_TIMEOUT_MILLISECONDS = 10000
|
export const GRADIDO_NODE_KILL_TIMEOUT_MILLISECONDS = 60 * 1000
|
||||||
// currently hard coded in gradido node, update in future
|
// currently hard coded in gradido node, update in future
|
||||||
export const GRADIDO_NODE_HOME_FOLDER_NAME = '.gradido'
|
export const GRADIDO_NODE_HOME_FOLDER_NAME = '.gradido'
|
||||||
|
|||||||
@ -82,14 +82,21 @@ export const configSchema = v.object({
|
|||||||
DLT_GRADIDO_NODE_SERVER_VERSION: v.optional(
|
DLT_GRADIDO_NODE_SERVER_VERSION: v.optional(
|
||||||
v.pipe(
|
v.pipe(
|
||||||
v.string('The version of the DLT node server, for example: 0.9.0'),
|
v.string('The version of the DLT node server, for example: 0.9.0'),
|
||||||
v.regex(/^\d+\.\d+\.\d+$/),
|
v.regex(/^\d+\.\d+\.\d+(.\d+)?$/),
|
||||||
),
|
),
|
||||||
'0.9.2',
|
'0.9.6.10',
|
||||||
),
|
),
|
||||||
DLT_GRADIDO_NODE_SERVER_HOME_FOLDER: v.optional(
|
DLT_GRADIDO_NODE_SERVER_HOME_FOLDER: v.optional(
|
||||||
v.string('The home folder for the gradido dlt node server'),
|
v.string('The home folder for the gradido dlt node server'),
|
||||||
path.join(__dirname, '..', '..', 'gradido_node'),
|
path.join(__dirname, '..', '..', 'gradido_node'),
|
||||||
),
|
),
|
||||||
|
DLT_GRADIDO_NODE_SERVER_ALLOW_CORS: v.optional(
|
||||||
|
v.pipe(
|
||||||
|
v.string('Whether to allow CORS for the gradido dlt node server'),
|
||||||
|
v.transform<string, boolean>((input: string) => input === 'true'),
|
||||||
|
),
|
||||||
|
'false',
|
||||||
|
),
|
||||||
BACKEND_PORT: v.optional(
|
BACKEND_PORT: v.optional(
|
||||||
v.pipe(
|
v.pipe(
|
||||||
v.string('A valid port on which the backend server is running'),
|
v.string('A valid port on which the backend server is running'),
|
||||||
|
|||||||
@ -54,6 +54,9 @@ export class KeyPairIdentifierLogic {
|
|||||||
return this.identifier.seed
|
return this.identifier.seed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getCommunityId(): Uuidv4 {
|
||||||
|
return this.identifier.communityId
|
||||||
|
}
|
||||||
getCommunityTopicId(): HieroId {
|
getCommunityTopicId(): HieroId {
|
||||||
return this.identifier.communityTopicId
|
return this.identifier.communityTopicId
|
||||||
}
|
}
|
||||||
@ -76,7 +79,7 @@ export class KeyPairIdentifierLogic {
|
|||||||
return this.getSeed()
|
return this.getSeed()
|
||||||
}
|
}
|
||||||
getCommunityKey(): string {
|
getCommunityKey(): string {
|
||||||
return this.getCommunityTopicId()
|
return this.getCommunityId()
|
||||||
}
|
}
|
||||||
getCommunityUserKey(): string {
|
getCommunityUserKey(): string {
|
||||||
return this.deriveCommunityUserHash(0)
|
return this.deriveCommunityUserHash(0)
|
||||||
@ -107,7 +110,7 @@ export class KeyPairIdentifierLogic {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
const resultString =
|
const resultString =
|
||||||
this.identifier.communityTopicId +
|
this.identifier.communityId +
|
||||||
this.identifier.account.userUuid.replace(/-/g, '') +
|
this.identifier.account.userUuid.replace(/-/g, '') +
|
||||||
accountNr.toString()
|
accountNr.toString()
|
||||||
return new MemoryBlock(resultString).calculateHash().convertToHex()
|
return new MemoryBlock(resultString).calculateHash().convertToHex()
|
||||||
|
|||||||
46
dlt-connector/src/data/deriveKeyPair.ts
Normal file
46
dlt-connector/src/data/deriveKeyPair.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@ -21,6 +21,9 @@ async function main() {
|
|||||||
|
|
||||||
// get home community, create topic if not exist, or check topic expiration and update it if needed
|
// get home community, create topic if not exist, or check topic expiration and update it if needed
|
||||||
const homeCommunity = await checkHomeCommunity(appContext, logger)
|
const homeCommunity = await checkHomeCommunity(appContext, logger)
|
||||||
|
if (!homeCommunity) {
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
// ask gradido node if community blockchain was created
|
// ask gradido node if community blockchain was created
|
||||||
// if not exist, create community root transaction
|
// if not exist, create community root transaction
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import { KeyPairEd25519 } from 'gradido-blockchain-js'
|
import { KeyPairEd25519 } from 'gradido-blockchain-js'
|
||||||
import { HieroId } from '../../schemas/typeGuard.schema'
|
import { Uuidv4 } from '../../schemas/typeGuard.schema'
|
||||||
|
|
||||||
export abstract class AbstractRemoteKeyPairRole {
|
export abstract class AbstractRemoteKeyPairRole {
|
||||||
protected topic: HieroId
|
protected communityId: Uuidv4
|
||||||
public constructor(communityTopicId: HieroId) {
|
public constructor(communityId: Uuidv4) {
|
||||||
this.topic = communityTopicId
|
this.communityId = communityId
|
||||||
}
|
}
|
||||||
public abstract retrieveKeyPair(): Promise<KeyPairEd25519>
|
public abstract retrieveKeyPair(): Promise<KeyPairEd25519>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,7 +10,7 @@ export class ForeignCommunityKeyPairRole extends AbstractRemoteKeyPairRole {
|
|||||||
public async retrieveKeyPair(): Promise<KeyPairEd25519> {
|
public async retrieveKeyPair(): Promise<KeyPairEd25519> {
|
||||||
const transactionIdentifier = {
|
const transactionIdentifier = {
|
||||||
transactionId: 1,
|
transactionId: 1,
|
||||||
topic: this.topic,
|
communityId: this.communityId,
|
||||||
}
|
}
|
||||||
const firstTransaction =
|
const firstTransaction =
|
||||||
await GradidoNodeClient.getInstance().getTransaction(transactionIdentifier)
|
await GradidoNodeClient.getInstance().getTransaction(transactionIdentifier)
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import { AbstractRemoteKeyPairRole } from './AbstractRemoteKeyPair.role'
|
|||||||
|
|
||||||
export class RemoteAccountKeyPairRole extends AbstractRemoteKeyPairRole {
|
export class RemoteAccountKeyPairRole extends AbstractRemoteKeyPairRole {
|
||||||
public constructor(private identifier: IdentifierAccount) {
|
public constructor(private identifier: IdentifierAccount) {
|
||||||
super(identifier.communityTopicId)
|
super(identifier.communityId)
|
||||||
}
|
}
|
||||||
|
|
||||||
public async retrieveKeyPair(): Promise<KeyPairEd25519> {
|
public async retrieveKeyPair(): Promise<KeyPairEd25519> {
|
||||||
@ -17,7 +17,7 @@ export class RemoteAccountKeyPairRole extends AbstractRemoteKeyPairRole {
|
|||||||
|
|
||||||
const accountPublicKey = await GradidoNodeClient.getInstance().findUserByNameHash(
|
const accountPublicKey = await GradidoNodeClient.getInstance().findUserByNameHash(
|
||||||
new Uuidv4Hash(this.identifier.account.userUuid),
|
new Uuidv4Hash(this.identifier.account.userUuid),
|
||||||
this.topic,
|
this.communityId,
|
||||||
)
|
)
|
||||||
if (accountPublicKey) {
|
if (accountPublicKey) {
|
||||||
return new KeyPairEd25519(MemoryBlock.createPtr(MemoryBlock.fromHex(accountPublicKey)))
|
return new KeyPairEd25519(MemoryBlock.createPtr(MemoryBlock.fromHex(accountPublicKey)))
|
||||||
|
|||||||
@ -33,6 +33,7 @@ mock.module('../../config', () => ({
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
const topicId = '0.0.21732'
|
const topicId = '0.0.21732'
|
||||||
|
const communityId = '1e88a0f4-d4fc-4cae-a7e8-a88e613ce324'
|
||||||
const userUuid = 'aa25cf6f-2879-4745-b2ea-6d3c37fb44b0'
|
const userUuid = 'aa25cf6f-2879-4745-b2ea-6d3c37fb44b0'
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
@ -45,7 +46,7 @@ describe('KeyPairCalculation', () => {
|
|||||||
})
|
})
|
||||||
it('community key pair', async () => {
|
it('community key pair', async () => {
|
||||||
const identifier = new KeyPairIdentifierLogic(
|
const identifier = new KeyPairIdentifierLogic(
|
||||||
v.parse(identifierKeyPairSchema, { communityTopicId: topicId }),
|
v.parse(identifierKeyPairSchema, { communityId, communityTopicId: topicId }),
|
||||||
)
|
)
|
||||||
const keyPair = await ResolveKeyPair(identifier)
|
const keyPair = await ResolveKeyPair(identifier)
|
||||||
expect(keyPair.getPublicKey()?.convertToHex()).toBe(
|
expect(keyPair.getPublicKey()?.convertToHex()).toBe(
|
||||||
@ -55,6 +56,7 @@ describe('KeyPairCalculation', () => {
|
|||||||
it('user key pair', async () => {
|
it('user key pair', async () => {
|
||||||
const identifier = new KeyPairIdentifierLogic(
|
const identifier = new KeyPairIdentifierLogic(
|
||||||
v.parse(identifierKeyPairSchema, {
|
v.parse(identifierKeyPairSchema, {
|
||||||
|
communityId,
|
||||||
communityTopicId: topicId,
|
communityTopicId: topicId,
|
||||||
account: { userUuid },
|
account: { userUuid },
|
||||||
}),
|
}),
|
||||||
@ -70,6 +72,7 @@ describe('KeyPairCalculation', () => {
|
|||||||
it('account key pair', async () => {
|
it('account key pair', async () => {
|
||||||
const identifier = new KeyPairIdentifierLogic(
|
const identifier = new KeyPairIdentifierLogic(
|
||||||
v.parse(identifierKeyPairSchema, {
|
v.parse(identifierKeyPairSchema, {
|
||||||
|
communityId,
|
||||||
communityTopicId: topicId,
|
communityTopicId: topicId,
|
||||||
account: { userUuid, accountNr: 1 },
|
account: { userUuid, accountNr: 1 },
|
||||||
}),
|
}),
|
||||||
|
|||||||
@ -45,7 +45,7 @@ export async function ResolveKeyPair(input: KeyPairIdentifierLogic): Promise<Key
|
|||||||
if (cache.getHomeCommunityTopicId() !== input.getCommunityTopicId()) {
|
if (cache.getHomeCommunityTopicId() !== input.getCommunityTopicId()) {
|
||||||
const role = input.isAccountKeyPair()
|
const role = input.isAccountKeyPair()
|
||||||
? new RemoteAccountKeyPairRole(input.identifier)
|
? new RemoteAccountKeyPairRole(input.identifier)
|
||||||
: new ForeignCommunityKeyPairRole(input.getCommunityTopicId())
|
: new ForeignCommunityKeyPairRole(input.getCommunityId())
|
||||||
return await role.retrieveKeyPair()
|
return await role.retrieveKeyPair()
|
||||||
}
|
}
|
||||||
// Community
|
// Community
|
||||||
|
|||||||
@ -27,7 +27,10 @@ export class CommunityRootTransactionRole extends AbstractTransactionRole {
|
|||||||
public async getGradidoTransactionBuilder(): Promise<GradidoTransactionBuilder> {
|
public async getGradidoTransactionBuilder(): Promise<GradidoTransactionBuilder> {
|
||||||
const builder = new GradidoTransactionBuilder()
|
const builder = new GradidoTransactionBuilder()
|
||||||
const communityKeyPair = await ResolveKeyPair(
|
const communityKeyPair = await ResolveKeyPair(
|
||||||
new KeyPairIdentifierLogic({ communityTopicId: this.community.hieroTopicId }),
|
new KeyPairIdentifierLogic({
|
||||||
|
communityTopicId: this.community.hieroTopicId,
|
||||||
|
communityId: this.community.uuid,
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
const gmwKeyPair = communityKeyPair.deriveChild(
|
const gmwKeyPair = communityKeyPair.deriveChild(
|
||||||
hardenDerivationIndex(GMW_ACCOUNT_DERIVATION_INDEX),
|
hardenDerivationIndex(GMW_ACCOUNT_DERIVATION_INDEX),
|
||||||
@ -47,6 +50,7 @@ export class CommunityRootTransactionRole extends AbstractTransactionRole {
|
|||||||
}
|
}
|
||||||
builder
|
builder
|
||||||
.setCreatedAt(this.community.creationDate)
|
.setCreatedAt(this.community.creationDate)
|
||||||
|
.setSenderCommunity(this.community.uuid)
|
||||||
.setCommunityRoot(
|
.setCommunityRoot(
|
||||||
communityKeyPair.getPublicKey(),
|
communityKeyPair.getPublicKey(),
|
||||||
gmwKeyPair.getPublicKey(),
|
gmwKeyPair.getPublicKey(),
|
||||||
|
|||||||
@ -36,7 +36,7 @@ export class CreationTransactionRole extends AbstractTransactionRole {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getRecipientCommunityTopicId(): HieroId {
|
getRecipientCommunityTopicId(): HieroId {
|
||||||
throw new Error('creation: cannot be used as cross group transaction')
|
return this.creationTransaction.user.communityTopicId
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getGradidoTransactionBuilder(): Promise<GradidoTransactionBuilder> {
|
public async getGradidoTransactionBuilder(): Promise<GradidoTransactionBuilder> {
|
||||||
@ -52,6 +52,7 @@ export class CreationTransactionRole extends AbstractTransactionRole {
|
|||||||
const homeCommunityKeyPair = await ResolveKeyPair(
|
const homeCommunityKeyPair = await ResolveKeyPair(
|
||||||
new KeyPairIdentifierLogic({
|
new KeyPairIdentifierLogic({
|
||||||
communityTopicId: this.homeCommunityTopicId,
|
communityTopicId: this.homeCommunityTopicId,
|
||||||
|
communityId: this.creationTransaction.user.communityId,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
// Memo: encrypted, home community and recipient can decrypt it
|
// Memo: encrypted, home community and recipient can decrypt it
|
||||||
@ -64,8 +65,13 @@ export class CreationTransactionRole extends AbstractTransactionRole {
|
|||||||
new AuthenticatedEncryption(recipientKeyPair),
|
new AuthenticatedEncryption(recipientKeyPair),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
.setRecipientCommunity(this.creationTransaction.user.communityId)
|
||||||
.setTransactionCreation(
|
.setTransactionCreation(
|
||||||
new TransferAmount(recipientKeyPair.getPublicKey(), this.creationTransaction.amount),
|
new TransferAmount(
|
||||||
|
recipientKeyPair.getPublicKey(),
|
||||||
|
this.creationTransaction.amount,
|
||||||
|
this.creationTransaction.user.communityId,
|
||||||
|
),
|
||||||
this.creationTransaction.targetDate,
|
this.creationTransaction.targetDate,
|
||||||
)
|
)
|
||||||
.sign(signerKeyPair)
|
.sign(signerKeyPair)
|
||||||
|
|||||||
@ -41,6 +41,7 @@ export class DeferredTransferTransactionRole extends AbstractTransactionRole {
|
|||||||
const recipientKeyPair = await ResolveKeyPair(
|
const recipientKeyPair = await ResolveKeyPair(
|
||||||
new KeyPairIdentifierLogic({
|
new KeyPairIdentifierLogic({
|
||||||
communityTopicId: this.deferredTransferTransaction.linkedUser.communityTopicId,
|
communityTopicId: this.deferredTransferTransaction.linkedUser.communityTopicId,
|
||||||
|
communityId: this.deferredTransferTransaction.linkedUser.communityId,
|
||||||
seed: this.seed,
|
seed: this.seed,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@ -54,6 +55,7 @@ export class DeferredTransferTransactionRole extends AbstractTransactionRole {
|
|||||||
new AuthenticatedEncryption(recipientKeyPair),
|
new AuthenticatedEncryption(recipientKeyPair),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
.setSenderCommunity(this.deferredTransferTransaction.user.communityId)
|
||||||
.setDeferredTransfer(
|
.setDeferredTransfer(
|
||||||
new GradidoTransfer(
|
new GradidoTransfer(
|
||||||
new TransferAmount(
|
new TransferAmount(
|
||||||
@ -61,6 +63,7 @@ export class DeferredTransferTransactionRole extends AbstractTransactionRole {
|
|||||||
this.deferredTransferTransaction.amount.calculateCompoundInterest(
|
this.deferredTransferTransaction.amount.calculateCompoundInterest(
|
||||||
this.deferredTransferTransaction.timeoutDuration.getSeconds(),
|
this.deferredTransferTransaction.timeoutDuration.getSeconds(),
|
||||||
),
|
),
|
||||||
|
this.deferredTransferTransaction.user.communityId,
|
||||||
),
|
),
|
||||||
recipientKeyPair.getPublicKey(),
|
recipientKeyPair.getPublicKey(),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -59,12 +59,15 @@ export class RedeemDeferredTransferTransactionRole extends AbstractTransactionRo
|
|||||||
|
|
||||||
builder
|
builder
|
||||||
.setCreatedAt(this.redeemDeferredTransferTransaction.createdAt)
|
.setCreatedAt(this.redeemDeferredTransferTransaction.createdAt)
|
||||||
|
.setSenderCommunity(this.redeemDeferredTransferTransaction.user.communityId)
|
||||||
|
.setRecipientCommunity(this.linkedUser.communityId)
|
||||||
.setRedeemDeferredTransfer(
|
.setRedeemDeferredTransfer(
|
||||||
this.parentDeferredTransaction.getId(),
|
this.parentDeferredTransaction.getId(),
|
||||||
new GradidoTransfer(
|
new GradidoTransfer(
|
||||||
new TransferAmount(
|
new TransferAmount(
|
||||||
senderKeyPair.getPublicKey(),
|
senderKeyPair.getPublicKey(),
|
||||||
this.redeemDeferredTransferTransaction.amount,
|
this.redeemDeferredTransferTransaction.amount,
|
||||||
|
this.redeemDeferredTransferTransaction.user.communityId,
|
||||||
),
|
),
|
||||||
recipientKeyPair.getPublicKey(),
|
recipientKeyPair.getPublicKey(),
|
||||||
),
|
),
|
||||||
@ -73,12 +76,6 @@ export class RedeemDeferredTransferTransactionRole extends AbstractTransactionRo
|
|||||||
for (let i = 0; i < memos.size(); i++) {
|
for (let i = 0; i < memos.size(); i++) {
|
||||||
builder.addMemo(memos.get(i))
|
builder.addMemo(memos.get(i))
|
||||||
}
|
}
|
||||||
const senderCommunity = this.redeemDeferredTransferTransaction.user.communityTopicId
|
|
||||||
const recipientCommunity = this.linkedUser.communityTopicId
|
|
||||||
if (senderCommunity !== recipientCommunity) {
|
|
||||||
// we have a cross group transaction
|
|
||||||
builder.setSenderCommunity(senderCommunity).setRecipientCommunity(recipientCommunity)
|
|
||||||
}
|
|
||||||
builder.sign(senderKeyPair)
|
builder.sign(senderKeyPair)
|
||||||
return builder
|
return builder
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,14 +1,20 @@
|
|||||||
import { describe, expect, it } from 'bun:test'
|
import { describe, expect, it } from 'bun:test'
|
||||||
import { InteractionValidate, ValidateType_SINGLE } from 'gradido-blockchain-js'
|
import {
|
||||||
|
InMemoryBlockchainProvider,
|
||||||
|
InteractionValidate,
|
||||||
|
ValidateType_SINGLE,
|
||||||
|
} from 'gradido-blockchain-js'
|
||||||
import * as v from 'valibot'
|
import * as v from 'valibot'
|
||||||
import { transactionSchema } from '../../schemas/transaction.schema'
|
import { transactionSchema } from '../../schemas/transaction.schema'
|
||||||
import { hieroIdSchema } from '../../schemas/typeGuard.schema'
|
import { hieroIdSchema } from '../../schemas/typeGuard.schema'
|
||||||
import { RegisterAddressTransactionRole } from './RegisterAddressTransaction.role'
|
import { RegisterAddressTransactionRole } from './RegisterAddressTransaction.role'
|
||||||
|
|
||||||
const userUuid = '408780b2-59b3-402a-94be-56a4f4f4e8ec'
|
const userUuid = '408780b2-59b3-402a-94be-56a4f4f4e8ec'
|
||||||
|
const communityId = '1e88a0f4-d4fc-4cae-a7e8-a88e613ce324'
|
||||||
const transaction = {
|
const transaction = {
|
||||||
user: {
|
user: {
|
||||||
communityTopicId: '0.0.21732',
|
communityTopicId: '0.0.21732',
|
||||||
|
communityId,
|
||||||
account: {
|
account: {
|
||||||
userUuid,
|
userUuid,
|
||||||
accountNr: 0,
|
accountNr: 0,
|
||||||
@ -18,6 +24,8 @@ const transaction = {
|
|||||||
accountType: 'COMMUNITY_HUMAN',
|
accountType: 'COMMUNITY_HUMAN',
|
||||||
createdAt: '2022-01-01T00:00:00.000Z',
|
createdAt: '2022-01-01T00:00:00.000Z',
|
||||||
}
|
}
|
||||||
|
// create blockchain in native module
|
||||||
|
InMemoryBlockchainProvider.getInstance().getBlockchain(communityId)
|
||||||
|
|
||||||
describe('RegisterAddressTransaction.role', () => {
|
describe('RegisterAddressTransaction.role', () => {
|
||||||
it('get correct prepared builder', async () => {
|
it('get correct prepared builder', async () => {
|
||||||
|
|||||||
@ -35,7 +35,12 @@ export class RegisterAddressTransactionRole extends AbstractTransactionRole {
|
|||||||
public async getGradidoTransactionBuilder(): Promise<GradidoTransactionBuilder> {
|
public async getGradidoTransactionBuilder(): Promise<GradidoTransactionBuilder> {
|
||||||
const builder = new GradidoTransactionBuilder()
|
const builder = new GradidoTransactionBuilder()
|
||||||
const communityTopicId = this.registerAddressTransaction.user.communityTopicId
|
const communityTopicId = this.registerAddressTransaction.user.communityTopicId
|
||||||
const communityKeyPair = await ResolveKeyPair(new KeyPairIdentifierLogic({ communityTopicId }))
|
const communityKeyPair = await ResolveKeyPair(
|
||||||
|
new KeyPairIdentifierLogic({
|
||||||
|
communityTopicId,
|
||||||
|
communityId: this.registerAddressTransaction.user.communityId,
|
||||||
|
}),
|
||||||
|
)
|
||||||
const keyPairIdentifier = this.registerAddressTransaction.user
|
const keyPairIdentifier = this.registerAddressTransaction.user
|
||||||
// when accountNr is 0 it is the user account
|
// when accountNr is 0 it is the user account
|
||||||
keyPairIdentifier.account.accountNr = 0
|
keyPairIdentifier.account.accountNr = 0
|
||||||
@ -45,6 +50,7 @@ export class RegisterAddressTransactionRole extends AbstractTransactionRole {
|
|||||||
|
|
||||||
builder
|
builder
|
||||||
.setCreatedAt(this.registerAddressTransaction.createdAt)
|
.setCreatedAt(this.registerAddressTransaction.createdAt)
|
||||||
|
.setSenderCommunity(this.registerAddressTransaction.user.communityId)
|
||||||
.setRegisterAddress(
|
.setRegisterAddress(
|
||||||
userKeyPair.getPublicKey(),
|
userKeyPair.getPublicKey(),
|
||||||
this.registerAddressTransaction.accountType as AddressType,
|
this.registerAddressTransaction.accountType as AddressType,
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import {
|
import {
|
||||||
GradidoTransaction,
|
GradidoTransaction,
|
||||||
HieroTransactionId,
|
HieroTransactionId,
|
||||||
InteractionSerialize,
|
|
||||||
InteractionValidate,
|
InteractionValidate,
|
||||||
|
LedgerAnchor,
|
||||||
ValidateType_SINGLE,
|
ValidateType_SINGLE,
|
||||||
} from 'gradido-blockchain-js'
|
} from 'gradido-blockchain-js'
|
||||||
import { getLogger } from 'log4js'
|
import { getLogger } from 'log4js'
|
||||||
@ -23,6 +23,8 @@ import {
|
|||||||
HieroTransactionIdString,
|
HieroTransactionIdString,
|
||||||
hieroTransactionIdStringSchema,
|
hieroTransactionIdStringSchema,
|
||||||
identifierSeedSchema,
|
identifierSeedSchema,
|
||||||
|
Uuidv4,
|
||||||
|
uuidv4Schema,
|
||||||
} from '../../schemas/typeGuard.schema'
|
} from '../../schemas/typeGuard.schema'
|
||||||
import { isTopicStillOpen } from '../../utils/hiero'
|
import { isTopicStillOpen } from '../../utils/hiero'
|
||||||
import { LinkedTransactionKeyPairRole } from '../resolveKeyPair/LinkedTransactionKeyPair.role'
|
import { LinkedTransactionKeyPairRole } from '../resolveKeyPair/LinkedTransactionKeyPair.role'
|
||||||
@ -59,20 +61,24 @@ export async function SendToHieroContext(
|
|||||||
const outboundHieroTransactionIdString = await sendViaHiero(
|
const outboundHieroTransactionIdString = await sendViaHiero(
|
||||||
outboundTransaction,
|
outboundTransaction,
|
||||||
role.getSenderCommunityTopicId(),
|
role.getSenderCommunityTopicId(),
|
||||||
|
v.parse(uuidv4Schema, outboundTransaction.getCommunityId()),
|
||||||
)
|
)
|
||||||
|
|
||||||
// serialize Hiero transaction ID and attach it to the builder for the inbound transaction
|
// attach Hiero transaction ID to the builder for the inbound transaction
|
||||||
const transactionIdSerializer = new InteractionSerialize(
|
builder.setParentLedgerAnchor(
|
||||||
new HieroTransactionId(outboundHieroTransactionIdString),
|
new LedgerAnchor(new HieroTransactionId(outboundHieroTransactionIdString)),
|
||||||
)
|
)
|
||||||
builder.setParentMessageId(transactionIdSerializer.run())
|
|
||||||
|
|
||||||
// build and validate inbound transaction
|
// build and validate inbound transaction
|
||||||
const inboundTransaction = builder.buildInbound()
|
const inboundTransaction = builder.buildInbound()
|
||||||
validate(inboundTransaction)
|
validate(inboundTransaction)
|
||||||
|
|
||||||
// send inbound transaction to hiero
|
// send inbound transaction to hiero
|
||||||
await sendViaHiero(inboundTransaction, role.getRecipientCommunityTopicId())
|
await sendViaHiero(
|
||||||
|
inboundTransaction,
|
||||||
|
role.getRecipientCommunityTopicId(),
|
||||||
|
v.parse(uuidv4Schema, inboundTransaction.getCommunityId()),
|
||||||
|
)
|
||||||
return outboundHieroTransactionIdString
|
return outboundHieroTransactionIdString
|
||||||
} else {
|
} else {
|
||||||
// build and validate local transaction
|
// build and validate local transaction
|
||||||
@ -83,6 +89,7 @@ export async function SendToHieroContext(
|
|||||||
const hieroTransactionIdString = await sendViaHiero(
|
const hieroTransactionIdString = await sendViaHiero(
|
||||||
transaction,
|
transaction,
|
||||||
role.getSenderCommunityTopicId(),
|
role.getSenderCommunityTopicId(),
|
||||||
|
v.parse(uuidv4Schema, transaction.getCommunityId()),
|
||||||
)
|
)
|
||||||
return hieroTransactionIdString
|
return hieroTransactionIdString
|
||||||
}
|
}
|
||||||
@ -98,9 +105,10 @@ function validate(transaction: GradidoTransaction): void {
|
|||||||
async function sendViaHiero(
|
async function sendViaHiero(
|
||||||
gradidoTransaction: GradidoTransaction,
|
gradidoTransaction: GradidoTransaction,
|
||||||
topic: HieroId,
|
topic: HieroId,
|
||||||
|
communityId: Uuidv4,
|
||||||
): Promise<HieroTransactionIdString> {
|
): Promise<HieroTransactionIdString> {
|
||||||
const client = HieroClient.getInstance()
|
const client = HieroClient.getInstance()
|
||||||
const transactionId = await client.sendMessage(topic, gradidoTransaction)
|
const transactionId = await client.sendMessage(topic, communityId, gradidoTransaction)
|
||||||
if (!transactionId) {
|
if (!transactionId) {
|
||||||
throw new Error('missing transaction id from hiero')
|
throw new Error('missing transaction id from hiero')
|
||||||
}
|
}
|
||||||
@ -156,7 +164,7 @@ async function chooseCorrectRole(
|
|||||||
throw new Error("redeem deferred transfer: couldn't generate seed public key")
|
throw new Error("redeem deferred transfer: couldn't generate seed public key")
|
||||||
}
|
}
|
||||||
const transactions = await GradidoNodeClient.getInstance().getTransactionsForAccount(
|
const transactions = await GradidoNodeClient.getInstance().getTransactionsForAccount(
|
||||||
{ maxResultCount: 2, topic: transaction.user.communityTopicId },
|
{ maxResultCount: 2, communityId: transaction.user.communityId },
|
||||||
seedPublicKey.convertToHex(),
|
seedPublicKey.convertToHex(),
|
||||||
)
|
)
|
||||||
if (!transactions || transactions.length !== 1) {
|
if (!transactions || transactions.length !== 1) {
|
||||||
|
|||||||
@ -40,7 +40,6 @@ export class TransferTransactionRole extends AbstractTransactionRole {
|
|||||||
const recipientKeyPair = await ResolveKeyPair(
|
const recipientKeyPair = await ResolveKeyPair(
|
||||||
new KeyPairIdentifierLogic(this.transferTransaction.linkedUser),
|
new KeyPairIdentifierLogic(this.transferTransaction.linkedUser),
|
||||||
)
|
)
|
||||||
|
|
||||||
builder
|
builder
|
||||||
.setCreatedAt(this.transferTransaction.createdAt)
|
.setCreatedAt(this.transferTransaction.createdAt)
|
||||||
.addMemo(
|
.addMemo(
|
||||||
@ -50,16 +49,16 @@ export class TransferTransactionRole extends AbstractTransactionRole {
|
|||||||
new AuthenticatedEncryption(recipientKeyPair),
|
new AuthenticatedEncryption(recipientKeyPair),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
.setSenderCommunity(this.transferTransaction.user.communityId)
|
||||||
|
.setRecipientCommunity(this.transferTransaction.linkedUser.communityId)
|
||||||
.setTransactionTransfer(
|
.setTransactionTransfer(
|
||||||
new TransferAmount(senderKeyPair.getPublicKey(), this.transferTransaction.amount),
|
new TransferAmount(
|
||||||
|
senderKeyPair.getPublicKey(),
|
||||||
|
this.transferTransaction.amount,
|
||||||
|
this.transferTransaction.user.communityId,
|
||||||
|
),
|
||||||
recipientKeyPair.getPublicKey(),
|
recipientKeyPair.getPublicKey(),
|
||||||
)
|
)
|
||||||
const senderCommunity = this.transferTransaction.user.communityTopicId
|
|
||||||
const recipientCommunity = this.transferTransaction.linkedUser.communityTopicId
|
|
||||||
if (senderCommunity !== recipientCommunity) {
|
|
||||||
// we have a cross group transaction
|
|
||||||
builder.setSenderCommunity(senderCommunity).setRecipientCommunity(recipientCommunity)
|
|
||||||
}
|
|
||||||
builder.sign(senderKeyPair)
|
builder.sign(senderKeyPair)
|
||||||
return builder
|
return builder
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,181 +0,0 @@
|
|||||||
import {
|
|
||||||
Filter,
|
|
||||||
GradidoTransactionBuilder,
|
|
||||||
HieroAccountId,
|
|
||||||
HieroTransactionId,
|
|
||||||
InMemoryBlockchain,
|
|
||||||
InteractionSerialize,
|
|
||||||
Timestamp,
|
|
||||||
} from 'gradido-blockchain-js'
|
|
||||||
import { getLogger } from 'log4js'
|
|
||||||
import * as v from 'valibot'
|
|
||||||
import { LOG4JS_BASE_CATEGORY } from '../../config/const'
|
|
||||||
import { InputTransactionType } from '../../data/InputTransactionType.enum'
|
|
||||||
import { LinkedTransactionKeyPairRole } from '../../interactions/resolveKeyPair/LinkedTransactionKeyPair.role'
|
|
||||||
import { CommunityRootTransactionRole } from '../../interactions/sendToHiero/CommunityRootTransaction.role'
|
|
||||||
import { CreationTransactionRole } from '../../interactions/sendToHiero/CreationTransaction.role'
|
|
||||||
import { DeferredTransferTransactionRole } from '../../interactions/sendToHiero/DeferredTransferTransaction.role'
|
|
||||||
import { RedeemDeferredTransferTransactionRole } from '../../interactions/sendToHiero/RedeemDeferredTransferTransaction.role'
|
|
||||||
import { RegisterAddressTransactionRole } from '../../interactions/sendToHiero/RegisterAddressTransaction.role'
|
|
||||||
import { TransferTransactionRole } from '../../interactions/sendToHiero/TransferTransaction.role'
|
|
||||||
import { Community, Transaction } from '../../schemas/transaction.schema'
|
|
||||||
import { identifierSeedSchema } from '../../schemas/typeGuard.schema'
|
|
||||||
|
|
||||||
const logger = getLogger(
|
|
||||||
`${LOG4JS_BASE_CATEGORY}.migrations.db-v2.7.0_to_blockchain-v3.6.blockchain`,
|
|
||||||
)
|
|
||||||
export const defaultHieroAccount = new HieroAccountId(0, 0, 2)
|
|
||||||
|
|
||||||
function addToBlockchain(
|
|
||||||
builder: GradidoTransactionBuilder,
|
|
||||||
blockchain: InMemoryBlockchain,
|
|
||||||
createdAtTimestamp: Timestamp,
|
|
||||||
): boolean {
|
|
||||||
const transaction = builder.build()
|
|
||||||
// TOD: use actual transaction id if exist in dlt_transactions table
|
|
||||||
const transactionId = new HieroTransactionId(createdAtTimestamp, defaultHieroAccount)
|
|
||||||
const interactionSerialize = new InteractionSerialize(transactionId)
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = blockchain.createAndAddConfirmedTransaction(
|
|
||||||
transaction,
|
|
||||||
interactionSerialize.run(),
|
|
||||||
createdAtTimestamp,
|
|
||||||
)
|
|
||||||
return result
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(`Transaction ${transaction.toJson(true)} not added: ${error}`)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function addCommunityRootTransaction(
|
|
||||||
blockchain: InMemoryBlockchain,
|
|
||||||
community: Community,
|
|
||||||
): Promise<void> {
|
|
||||||
const communityRootTransactionRole = new CommunityRootTransactionRole(community)
|
|
||||||
if (
|
|
||||||
addToBlockchain(
|
|
||||||
await communityRootTransactionRole.getGradidoTransactionBuilder(),
|
|
||||||
blockchain,
|
|
||||||
new Timestamp(community.creationDate),
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
logger.info(`Community Root Transaction added`)
|
|
||||||
} else {
|
|
||||||
throw new Error(`Community Root Transaction not added`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function addRegisterAddressTransaction(
|
|
||||||
blockchain: InMemoryBlockchain,
|
|
||||||
transaction: Transaction,
|
|
||||||
): Promise<void> {
|
|
||||||
const registerAddressRole = new RegisterAddressTransactionRole(transaction)
|
|
||||||
if (
|
|
||||||
addToBlockchain(
|
|
||||||
await registerAddressRole.getGradidoTransactionBuilder(),
|
|
||||||
blockchain,
|
|
||||||
new Timestamp(transaction.createdAt),
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
logger.debug(
|
|
||||||
`Register Address Transaction added for user ${transaction.user.account!.userUuid}`,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
throw new Error(
|
|
||||||
`Register Address Transaction not added for user ${transaction.user.account!.userUuid}`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function addTransaction(
|
|
||||||
senderBlockchain: InMemoryBlockchain,
|
|
||||||
_recipientBlockchain: InMemoryBlockchain,
|
|
||||||
transaction: Transaction,
|
|
||||||
): Promise<void> {
|
|
||||||
const createdAtTimestamp = new Timestamp(transaction.createdAt)
|
|
||||||
if (transaction.type === InputTransactionType.GRADIDO_CREATION) {
|
|
||||||
const creationTransactionRole = new CreationTransactionRole(transaction)
|
|
||||||
if (
|
|
||||||
addToBlockchain(
|
|
||||||
await creationTransactionRole.getGradidoTransactionBuilder(),
|
|
||||||
senderBlockchain,
|
|
||||||
createdAtTimestamp,
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
logger.debug(`Creation Transaction added for user ${transaction.user.account!.userUuid}`)
|
|
||||||
} else {
|
|
||||||
throw new Error(
|
|
||||||
`Creation Transaction not added for user ${transaction.user.account!.userUuid}`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else if (transaction.type === InputTransactionType.GRADIDO_TRANSFER) {
|
|
||||||
const transferTransactionRole = new TransferTransactionRole(transaction)
|
|
||||||
// will crash with cross group transaction
|
|
||||||
if (
|
|
||||||
addToBlockchain(
|
|
||||||
await transferTransactionRole.getGradidoTransactionBuilder(),
|
|
||||||
senderBlockchain,
|
|
||||||
createdAtTimestamp,
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
logger.debug(`Transfer Transaction added for user ${transaction.user.account!.userUuid}`)
|
|
||||||
} else {
|
|
||||||
throw new Error(
|
|
||||||
`Transfer Transaction not added for user ${transaction.user.account!.userUuid}`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else if (transaction.type === InputTransactionType.GRADIDO_DEFERRED_TRANSFER) {
|
|
||||||
const transferTransactionRole = new DeferredTransferTransactionRole(transaction)
|
|
||||||
if (
|
|
||||||
addToBlockchain(
|
|
||||||
await transferTransactionRole.getGradidoTransactionBuilder(),
|
|
||||||
senderBlockchain,
|
|
||||||
createdAtTimestamp,
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
logger.debug(
|
|
||||||
`Deferred Transfer Transaction added for user ${transaction.user.account!.userUuid}`,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
throw new Error(
|
|
||||||
`Deferred Transfer Transaction not added for user ${transaction.user.account!.userUuid}`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else if (transaction.type === InputTransactionType.GRADIDO_REDEEM_DEFERRED_TRANSFER) {
|
|
||||||
const seedKeyPairRole = new LinkedTransactionKeyPairRole(
|
|
||||||
v.parse(identifierSeedSchema, transaction.user.seed),
|
|
||||||
)
|
|
||||||
const f = new Filter()
|
|
||||||
f.involvedPublicKey = seedKeyPairRole.generateKeyPair().getPublicKey()
|
|
||||||
const deferredTransaction = senderBlockchain.findOne(f)
|
|
||||||
if (!deferredTransaction) {
|
|
||||||
throw new Error(
|
|
||||||
`redeem deferred transfer: couldn't find parent deferred transfer on Gradido Node for ${JSON.stringify(transaction, null, 2)} and public key from seed: ${f.involvedPublicKey?.convertToHex()}`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
const confirmedDeferredTransaction = deferredTransaction.getConfirmedTransaction()
|
|
||||||
if (!confirmedDeferredTransaction) {
|
|
||||||
throw new Error('redeem deferred transfer: invalid TransactionEntry')
|
|
||||||
}
|
|
||||||
const redeemTransactionRole = new RedeemDeferredTransferTransactionRole(
|
|
||||||
transaction,
|
|
||||||
confirmedDeferredTransaction,
|
|
||||||
)
|
|
||||||
const involvedUser = transaction.user.account
|
|
||||||
? transaction.user.account.userUuid
|
|
||||||
: transaction.linkedUser?.account?.userUuid
|
|
||||||
if (
|
|
||||||
addToBlockchain(
|
|
||||||
await redeemTransactionRole.getGradidoTransactionBuilder(),
|
|
||||||
senderBlockchain,
|
|
||||||
createdAtTimestamp,
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
logger.debug(`Redeem Deferred Transfer Transaction added for user ${involvedUser}`)
|
|
||||||
} else {
|
|
||||||
throw new Error(`Redeem Deferred Transfer Transaction not added for user ${involvedUser}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,55 +0,0 @@
|
|||||||
import { InMemoryBlockchainProvider } from 'gradido-blockchain-js'
|
|
||||||
import * as v from 'valibot'
|
|
||||||
import { HieroId, hieroIdSchema } from '../../schemas/typeGuard.schema'
|
|
||||||
import { addCommunityRootTransaction } from './blockchain'
|
|
||||||
import { Context } from './Context'
|
|
||||||
import { communityDbToCommunity } from './convert'
|
|
||||||
import { loadCommunities } from './database'
|
|
||||||
import { generateKeyPairCommunity } from './keyPair'
|
|
||||||
import { CommunityContext } from './valibot.schema'
|
|
||||||
|
|
||||||
export async function bootstrap(): Promise<Context> {
|
|
||||||
const context = await Context.create()
|
|
||||||
context.communities = await bootstrapCommunities(context)
|
|
||||||
return context
|
|
||||||
}
|
|
||||||
|
|
||||||
async function bootstrapCommunities(context: Context): Promise<Map<string, CommunityContext>> {
|
|
||||||
const communities = new Map<string, CommunityContext>()
|
|
||||||
const communitiesDb = await loadCommunities(context.db)
|
|
||||||
const topicIds = new Set<HieroId>()
|
|
||||||
|
|
||||||
for (const communityDb of communitiesDb) {
|
|
||||||
const blockchain = InMemoryBlockchainProvider.getInstance().findBlockchain(
|
|
||||||
communityDb.uniqueAlias,
|
|
||||||
)
|
|
||||||
if (!blockchain) {
|
|
||||||
throw new Error(`Couldn't create Blockchain for community ${communityDb.communityUuid}`)
|
|
||||||
}
|
|
||||||
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, '_'),
|
|
||||||
})
|
|
||||||
|
|
||||||
generateKeyPairCommunity(communityDb, context.cache, topicId)
|
|
||||||
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)
|
|
||||||
await addCommunityRootTransaction(blockchain, community)
|
|
||||||
}
|
|
||||||
return communities
|
|
||||||
}
|
|
||||||
@ -1,136 +0,0 @@
|
|||||||
import * as v from 'valibot'
|
|
||||||
import { AccountType } from '../../data/AccountType.enum'
|
|
||||||
import { InputTransactionType } from '../../data/InputTransactionType.enum'
|
|
||||||
import {
|
|
||||||
Community,
|
|
||||||
communitySchema,
|
|
||||||
Transaction,
|
|
||||||
TransactionInput,
|
|
||||||
transactionSchema,
|
|
||||||
} from '../../schemas/transaction.schema'
|
|
||||||
import {
|
|
||||||
gradidoAmountSchema,
|
|
||||||
HieroId,
|
|
||||||
memoSchema,
|
|
||||||
timeoutDurationSchema,
|
|
||||||
} from '../../schemas/typeGuard.schema'
|
|
||||||
import { TransactionTypeId } from './TransactionTypeId'
|
|
||||||
import { CommunityDb, CreatedUserDb, TransactionDb, TransactionLinkDb } from './valibot.schema'
|
|
||||||
|
|
||||||
export function getInputTransactionTypeFromTypeId(typeId: TransactionTypeId): InputTransactionType {
|
|
||||||
switch (typeId) {
|
|
||||||
case TransactionTypeId.CREATION:
|
|
||||||
return InputTransactionType.GRADIDO_CREATION
|
|
||||||
case TransactionTypeId.SEND:
|
|
||||||
return InputTransactionType.GRADIDO_TRANSFER
|
|
||||||
case TransactionTypeId.RECEIVE:
|
|
||||||
throw new Error('not used')
|
|
||||||
default:
|
|
||||||
throw new Error('not implemented')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function communityDbToCommunity(
|
|
||||||
topicId: HieroId,
|
|
||||||
communityDb: CommunityDb,
|
|
||||||
creationDate: Date,
|
|
||||||
): Community {
|
|
||||||
return v.parse(communitySchema, {
|
|
||||||
hieroTopicId: topicId,
|
|
||||||
uuid: communityDb.communityUuid,
|
|
||||||
foreign: communityDb.foreign,
|
|
||||||
creationDate,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function userDbToTransaction(userDb: CreatedUserDb, communityTopicId: HieroId): Transaction {
|
|
||||||
return v.parse(transactionSchema, {
|
|
||||||
user: {
|
|
||||||
communityTopicId: communityTopicId,
|
|
||||||
account: { userUuid: userDb.gradidoId },
|
|
||||||
},
|
|
||||||
type: InputTransactionType.REGISTER_ADDRESS,
|
|
||||||
accountType: AccountType.COMMUNITY_HUMAN,
|
|
||||||
createdAt: userDb.createdAt,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function transactionDbToTransaction(
|
|
||||||
transactionDb: TransactionDb,
|
|
||||||
communityTopicId: HieroId,
|
|
||||||
recipientCommunityTopicId: HieroId,
|
|
||||||
): Transaction {
|
|
||||||
if (
|
|
||||||
transactionDb.typeId !== TransactionTypeId.CREATION &&
|
|
||||||
transactionDb.typeId !== TransactionTypeId.SEND &&
|
|
||||||
transactionDb.typeId !== TransactionTypeId.RECEIVE
|
|
||||||
) {
|
|
||||||
throw new Error('not implemented')
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = {
|
|
||||||
communityTopicId: communityTopicId,
|
|
||||||
account: { userUuid: transactionDb.user.gradidoId },
|
|
||||||
}
|
|
||||||
const linkedUser = {
|
|
||||||
communityTopicId: recipientCommunityTopicId,
|
|
||||||
account: { userUuid: transactionDb.linkedUser.gradidoId },
|
|
||||||
}
|
|
||||||
const transaction: TransactionInput = {
|
|
||||||
user,
|
|
||||||
linkedUser,
|
|
||||||
amount: v.parse(gradidoAmountSchema, transactionDb.amount),
|
|
||||||
memo: v.parse(memoSchema, transactionDb.memo),
|
|
||||||
type: InputTransactionType.GRADIDO_TRANSFER,
|
|
||||||
createdAt: transactionDb.balanceDate,
|
|
||||||
}
|
|
||||||
if (transactionDb.typeId === TransactionTypeId.CREATION) {
|
|
||||||
if (!transactionDb.creationDate) {
|
|
||||||
throw new Error('contribution transaction without creation date')
|
|
||||||
}
|
|
||||||
transaction.targetDate = transactionDb.creationDate
|
|
||||||
transaction.type = InputTransactionType.GRADIDO_CREATION
|
|
||||||
} else if (transactionDb.typeId === TransactionTypeId.RECEIVE) {
|
|
||||||
transaction.user = linkedUser
|
|
||||||
transaction.linkedUser = user
|
|
||||||
}
|
|
||||||
if (transactionDb.transactionLinkCode) {
|
|
||||||
if (transactionDb.typeId !== TransactionTypeId.RECEIVE) {
|
|
||||||
throw new Error(
|
|
||||||
"linked transaction which isn't receive, send will taken care of on link creation",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
transaction.user = {
|
|
||||||
communityTopicId: recipientCommunityTopicId,
|
|
||||||
seed: transactionDb.transactionLinkCode,
|
|
||||||
}
|
|
||||||
transaction.type = InputTransactionType.GRADIDO_REDEEM_DEFERRED_TRANSFER
|
|
||||||
}
|
|
||||||
return v.parse(transactionSchema, transaction)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function transactionLinkDbToTransaction(
|
|
||||||
transactionLinkDb: TransactionLinkDb,
|
|
||||||
communityTopicId: HieroId,
|
|
||||||
): Transaction {
|
|
||||||
return v.parse(transactionSchema, {
|
|
||||||
user: {
|
|
||||||
communityTopicId: communityTopicId,
|
|
||||||
account: { userUuid: transactionLinkDb.user.gradidoId },
|
|
||||||
},
|
|
||||||
linkedUser: {
|
|
||||||
communityTopicId: communityTopicId,
|
|
||||||
seed: transactionLinkDb.code,
|
|
||||||
},
|
|
||||||
type: InputTransactionType.GRADIDO_DEFERRED_TRANSFER,
|
|
||||||
amount: v.parse(gradidoAmountSchema, transactionLinkDb.amount),
|
|
||||||
memo: v.parse(memoSchema, transactionLinkDb.memo),
|
|
||||||
createdAt: transactionLinkDb.createdAt,
|
|
||||||
timeoutDuration: v.parse(
|
|
||||||
timeoutDurationSchema,
|
|
||||||
Math.round(
|
|
||||||
(transactionLinkDb.validUntil.getTime() - transactionLinkDb.createdAt.getTime()) / 1000,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@ -1,189 +0,0 @@
|
|||||||
import { asc, eq, inArray, isNotNull, sql } from 'drizzle-orm'
|
|
||||||
import { alias } from 'drizzle-orm/mysql-core'
|
|
||||||
import { MySql2Database } from 'drizzle-orm/mysql2'
|
|
||||||
import { GradidoUnit } from 'gradido-blockchain-js'
|
|
||||||
import { getLogger } from 'log4js'
|
|
||||||
import * as v from 'valibot'
|
|
||||||
import { LOG4JS_BASE_CATEGORY } from '../../config/const'
|
|
||||||
import {
|
|
||||||
communitiesTable,
|
|
||||||
transactionLinksTable,
|
|
||||||
transactionsTable,
|
|
||||||
usersTable,
|
|
||||||
} from './drizzle.schema'
|
|
||||||
import { TransactionTypeId } from './TransactionTypeId'
|
|
||||||
import {
|
|
||||||
CommunityDb,
|
|
||||||
CreatedUserDb,
|
|
||||||
communityDbSchema,
|
|
||||||
createdUserDbSchema,
|
|
||||||
TransactionDb,
|
|
||||||
TransactionLinkDb,
|
|
||||||
transactionDbSchema,
|
|
||||||
transactionLinkDbSchema,
|
|
||||||
} from './valibot.schema'
|
|
||||||
|
|
||||||
const logger = getLogger(
|
|
||||||
`${LOG4JS_BASE_CATEGORY}.migrations.db-v2.7.0_to_blockchain-v3.6.blockchain`,
|
|
||||||
)
|
|
||||||
|
|
||||||
// queries
|
|
||||||
export async function loadCommunities(db: MySql2Database): Promise<CommunityDb[]> {
|
|
||||||
const result = await db
|
|
||||||
.select({
|
|
||||||
foreign: communitiesTable.foreign,
|
|
||||||
communityUuid: communitiesTable.communityUuid,
|
|
||||||
name: communitiesTable.name,
|
|
||||||
creationDate: communitiesTable.creationDate,
|
|
||||||
userMinCreatedAt: sql`MIN(${usersTable.createdAt})`,
|
|
||||||
})
|
|
||||||
.from(communitiesTable)
|
|
||||||
.leftJoin(usersTable, eq(communitiesTable.communityUuid, usersTable.communityUuid))
|
|
||||||
.where(isNotNull(communitiesTable.communityUuid))
|
|
||||||
.orderBy(asc(communitiesTable.id))
|
|
||||||
.groupBy(communitiesTable.communityUuid)
|
|
||||||
|
|
||||||
const communityNames = new Set<string>()
|
|
||||||
return result.map((row: any) => {
|
|
||||||
let alias = row.name
|
|
||||||
if (communityNames.has(row.name)) {
|
|
||||||
alias = row.community_uuid
|
|
||||||
} else {
|
|
||||||
communityNames.add(row.name)
|
|
||||||
}
|
|
||||||
return v.parse(communityDbSchema, {
|
|
||||||
...row,
|
|
||||||
uniqueAlias: alias,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function loadUsers(
|
|
||||||
db: MySql2Database,
|
|
||||||
offset: number,
|
|
||||||
count: number,
|
|
||||||
): Promise<CreatedUserDb[]> {
|
|
||||||
const result = await db
|
|
||||||
.select()
|
|
||||||
.from(usersTable)
|
|
||||||
.orderBy(asc(usersTable.createdAt), asc(usersTable.id))
|
|
||||||
.limit(count)
|
|
||||||
.offset(offset)
|
|
||||||
|
|
||||||
return result.map((row: any) => {
|
|
||||||
return v.parse(createdUserDbSchema, row)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function loadTransactions(
|
|
||||||
db: MySql2Database,
|
|
||||||
offset: number,
|
|
||||||
count: number,
|
|
||||||
): Promise<TransactionDb[]> {
|
|
||||||
const linkedUsers = alias(usersTable, 'linkedUser')
|
|
||||||
|
|
||||||
const result = await db
|
|
||||||
.select({
|
|
||||||
transaction: transactionsTable,
|
|
||||||
user: usersTable,
|
|
||||||
linkedUser: linkedUsers,
|
|
||||||
transactionLink: transactionLinksTable,
|
|
||||||
})
|
|
||||||
.from(transactionsTable)
|
|
||||||
.where(
|
|
||||||
inArray(transactionsTable.typeId, [TransactionTypeId.CREATION, TransactionTypeId.RECEIVE]),
|
|
||||||
)
|
|
||||||
.leftJoin(usersTable, eq(transactionsTable.userId, usersTable.id))
|
|
||||||
.leftJoin(linkedUsers, eq(transactionsTable.linkedUserId, linkedUsers.id))
|
|
||||||
.leftJoin(
|
|
||||||
transactionLinksTable,
|
|
||||||
eq(transactionsTable.transactionLinkId, transactionLinksTable.id),
|
|
||||||
)
|
|
||||||
.orderBy(asc(transactionsTable.balanceDate), asc(transactionsTable.id))
|
|
||||||
.limit(count)
|
|
||||||
.offset(offset)
|
|
||||||
|
|
||||||
return result.map((row: any) => {
|
|
||||||
// console.log(row)
|
|
||||||
try {
|
|
||||||
// check for consistent data beforehand
|
|
||||||
const userCreatedAt = new Date(row.user.createdAt)
|
|
||||||
const linkedUserCreatedAd = new Date(row.linkedUser.createdAt)
|
|
||||||
const balanceDate = new Date(row.transaction.balanceDate)
|
|
||||||
if (
|
|
||||||
userCreatedAt.getTime() > balanceDate.getTime() ||
|
|
||||||
linkedUserCreatedAd.getTime() > balanceDate.getTime()
|
|
||||||
) {
|
|
||||||
logger.error(`table row: `, row)
|
|
||||||
throw new Error(
|
|
||||||
'at least one user was created after transaction balance date, logic error!',
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
let amount = GradidoUnit.fromString(row.transaction.amount)
|
|
||||||
if (row.transaction.typeId === TransactionTypeId.SEND) {
|
|
||||||
amount = amount.mul(new GradidoUnit(-1))
|
|
||||||
}
|
|
||||||
return v.parse(transactionDbSchema, {
|
|
||||||
...row.transaction,
|
|
||||||
transactionLinkCode: row.transactionLink ? row.transactionLink.code : null,
|
|
||||||
user: row.user,
|
|
||||||
linkedUser: row.linkedUser,
|
|
||||||
})
|
|
||||||
} catch (e) {
|
|
||||||
logger.error(`table row: ${JSON.stringify(row, null, 2)}`)
|
|
||||||
if (e instanceof v.ValiError) {
|
|
||||||
logger.error(v.flatten(e.issues))
|
|
||||||
}
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function loadTransactionLinks(
|
|
||||||
db: MySql2Database,
|
|
||||||
offset: number,
|
|
||||||
count: number,
|
|
||||||
): Promise<TransactionLinkDb[]> {
|
|
||||||
const result = await db
|
|
||||||
.select()
|
|
||||||
.from(transactionLinksTable)
|
|
||||||
.leftJoin(usersTable, eq(transactionLinksTable.userId, usersTable.id))
|
|
||||||
.orderBy(asc(transactionLinksTable.createdAt), asc(transactionLinksTable.id))
|
|
||||||
.limit(count)
|
|
||||||
.offset(offset)
|
|
||||||
|
|
||||||
return result.map((row: any) => {
|
|
||||||
return v.parse(transactionLinkDbSchema, {
|
|
||||||
...row.transaction_links,
|
|
||||||
user: row.users,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function loadDeletedTransactionLinks(
|
|
||||||
db: MySql2Database,
|
|
||||||
offset: number,
|
|
||||||
count: number,
|
|
||||||
): Promise<TransactionDb[]> {
|
|
||||||
const result = await db
|
|
||||||
.select()
|
|
||||||
.from(transactionLinksTable)
|
|
||||||
.leftJoin(usersTable, eq(transactionLinksTable.userId, usersTable.id))
|
|
||||||
.where(isNotNull(transactionLinksTable.deletedAt))
|
|
||||||
.orderBy(asc(transactionLinksTable.deletedAt), asc(transactionLinksTable.id))
|
|
||||||
.limit(count)
|
|
||||||
.offset(offset)
|
|
||||||
|
|
||||||
return result.map((row: any) => {
|
|
||||||
return v.parse(transactionDbSchema, {
|
|
||||||
typeId: TransactionTypeId.RECEIVE,
|
|
||||||
amount: row.transaction_links.amount,
|
|
||||||
balanceDate: new Date(row.transaction_links.deletedAt),
|
|
||||||
memo: row.transaction_links.memo,
|
|
||||||
transactionLinkCode: row.transaction_links.code,
|
|
||||||
user: row.users,
|
|
||||||
linkedUser: row.users,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@ -1,36 +0,0 @@
|
|||||||
import { Filter } from 'gradido-blockchain-js'
|
|
||||||
import { onShutdown } from '../../../../shared/src/helper/onShutdown'
|
|
||||||
import { exportAllCommunities } from './binaryExport'
|
|
||||||
import { bootstrap } from './bootstrap'
|
|
||||||
import { syncDbWithBlockchainContext } from './interaction/syncDbWithBlockchain/syncDbWithBlockchain.context'
|
|
||||||
|
|
||||||
const BATCH_SIZE = 100
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
// prepare in memory blockchains
|
|
||||||
const context = await bootstrap()
|
|
||||||
onShutdown(async (reason, error) => {
|
|
||||||
context.logger.info(`shutdown reason: ${reason}`)
|
|
||||||
if (error) {
|
|
||||||
context.logger.error(error)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// synchronize to in memory blockchain
|
|
||||||
await syncDbWithBlockchainContext(context, BATCH_SIZE)
|
|
||||||
|
|
||||||
// write as binary file for GradidoNode
|
|
||||||
exportAllCommunities(context, BATCH_SIZE)
|
|
||||||
|
|
||||||
// log runtime statistics
|
|
||||||
context.logRuntimeStatistics()
|
|
||||||
|
|
||||||
// needed because of shutdown handler (TODO: fix shutdown handler)
|
|
||||||
process.exit(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch((e) => {
|
|
||||||
// biome-ignore lint/suspicious/noConsole: maybe logger isn't initialized here
|
|
||||||
console.error(e)
|
|
||||||
process.exit(1)
|
|
||||||
})
|
|
||||||
@ -1,70 +0,0 @@
|
|||||||
import { Profiler } from 'gradido-blockchain-js'
|
|
||||||
import { getLogger, Logger } from 'log4js'
|
|
||||||
import { LOG4JS_BASE_CATEGORY } from '../../../../config/const'
|
|
||||||
import { Context } from '../../Context'
|
|
||||||
|
|
||||||
export abstract class AbstractSyncRole<T> {
|
|
||||||
private items: T[] = []
|
|
||||||
private offset = 0
|
|
||||||
protected logger: Logger
|
|
||||||
|
|
||||||
constructor(protected readonly context: Context) {
|
|
||||||
this.logger = getLogger(
|
|
||||||
`${LOG4JS_BASE_CATEGORY}.migrations.db-v2.7.0_to_blockchain-v3.5.interaction.syncDbWithBlockchain`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract getDate(): Date
|
|
||||||
abstract loadFromDb(offset: number, count: number): Promise<T[]>
|
|
||||||
abstract pushToBlockchain(item: T): Promise<void>
|
|
||||||
abstract itemTypeName(): string
|
|
||||||
|
|
||||||
// return count of new loaded items
|
|
||||||
async ensureFilled(batchSize: number): Promise<number> {
|
|
||||||
if (this.items.length === 0) {
|
|
||||||
let timeUsed: Profiler | undefined
|
|
||||||
if (this.logger.isDebugEnabled()) {
|
|
||||||
timeUsed = new Profiler()
|
|
||||||
}
|
|
||||||
this.items = await this.loadFromDb(this.offset, batchSize)
|
|
||||||
this.offset += this.items.length
|
|
||||||
if (timeUsed && this.items.length) {
|
|
||||||
this.logger.debug(
|
|
||||||
`${timeUsed.string()} for loading ${this.items.length} ${this.itemTypeName()} from db`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return this.items.length
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
async toBlockchain(): Promise<void> {
|
|
||||||
if (this.isEmpty()) {
|
|
||||||
throw new Error(`[toBlockchain] No items, please call this only if isEmpty returns false`)
|
|
||||||
}
|
|
||||||
await this.pushToBlockchain(this.shift())
|
|
||||||
}
|
|
||||||
|
|
||||||
peek(): T {
|
|
||||||
if (this.isEmpty()) {
|
|
||||||
throw new Error(`[peek] No items, please call this only if isEmpty returns false`)
|
|
||||||
}
|
|
||||||
return this.items[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
shift(): T {
|
|
||||||
const item = this.items.shift()
|
|
||||||
if (!item) {
|
|
||||||
throw new Error(`[shift] No items, shift return undefined`)
|
|
||||||
}
|
|
||||||
return item
|
|
||||||
}
|
|
||||||
|
|
||||||
get length(): number {
|
|
||||||
return this.items.length
|
|
||||||
}
|
|
||||||
|
|
||||||
isEmpty(): boolean {
|
|
||||||
return this.items.length === 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
import { loadDeletedTransactionLinks } from '../../database'
|
|
||||||
import { TransactionDb } from '../../valibot.schema'
|
|
||||||
import { TransactionsSyncRole } from './TransactionsSync.role'
|
|
||||||
|
|
||||||
export class DeletedTransactionLinksSyncRole extends TransactionsSyncRole {
|
|
||||||
itemTypeName(): string {
|
|
||||||
return 'deletedTransactionLinks'
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadFromDb(offset: number, count: number): Promise<TransactionDb[]> {
|
|
||||||
return await loadDeletedTransactionLinks(this.context.db, offset, count)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,25 +0,0 @@
|
|||||||
import { addTransaction } from '../../blockchain'
|
|
||||||
import { transactionLinkDbToTransaction } from '../../convert'
|
|
||||||
import { loadTransactionLinks } from '../../database'
|
|
||||||
import { TransactionLinkDb } from '../../valibot.schema'
|
|
||||||
import { AbstractSyncRole } from './AbstractSync.role'
|
|
||||||
|
|
||||||
export class TransactionLinksSyncRole extends AbstractSyncRole<TransactionLinkDb> {
|
|
||||||
getDate(): Date {
|
|
||||||
return this.peek().createdAt
|
|
||||||
}
|
|
||||||
|
|
||||||
itemTypeName(): string {
|
|
||||||
return 'transactionLinks'
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadFromDb(offset: number, count: number): Promise<TransactionLinkDb[]> {
|
|
||||||
return await loadTransactionLinks(this.context.db, offset, count)
|
|
||||||
}
|
|
||||||
|
|
||||||
async pushToBlockchain(item: TransactionLinkDb): Promise<void> {
|
|
||||||
const communityContext = this.context.getCommunityContextByUuid(item.user.communityUuid)
|
|
||||||
const transaction = transactionLinkDbToTransaction(item, communityContext.topicId)
|
|
||||||
await addTransaction(communityContext.blockchain, communityContext.blockchain, transaction)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,37 +0,0 @@
|
|||||||
import { addTransaction } from '../../blockchain'
|
|
||||||
import { transactionDbToTransaction } from '../../convert'
|
|
||||||
import { loadTransactions } from '../../database'
|
|
||||||
import { TransactionDb } from '../../valibot.schema'
|
|
||||||
import { AbstractSyncRole } from './AbstractSync.role'
|
|
||||||
|
|
||||||
export class TransactionsSyncRole extends AbstractSyncRole<TransactionDb> {
|
|
||||||
getDate(): Date {
|
|
||||||
return this.peek().balanceDate
|
|
||||||
}
|
|
||||||
|
|
||||||
itemTypeName(): string {
|
|
||||||
return 'transactions'
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadFromDb(offset: number, count: number): Promise<TransactionDb[]> {
|
|
||||||
return await loadTransactions(this.context.db, offset, count)
|
|
||||||
}
|
|
||||||
|
|
||||||
async pushToBlockchain(item: TransactionDb): Promise<void> {
|
|
||||||
const senderCommunityContext = this.context.getCommunityContextByUuid(item.user.communityUuid)
|
|
||||||
const recipientCommunityContext = this.context.getCommunityContextByUuid(
|
|
||||||
item.linkedUser.communityUuid,
|
|
||||||
)
|
|
||||||
this.context.cache.setHomeCommunityTopicId(senderCommunityContext.topicId)
|
|
||||||
const transaction = transactionDbToTransaction(
|
|
||||||
item,
|
|
||||||
senderCommunityContext.topicId,
|
|
||||||
recipientCommunityContext.topicId,
|
|
||||||
)
|
|
||||||
await addTransaction(
|
|
||||||
senderCommunityContext.blockchain,
|
|
||||||
recipientCommunityContext.blockchain,
|
|
||||||
transaction,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,31 +0,0 @@
|
|||||||
import { addRegisterAddressTransaction } from '../../blockchain'
|
|
||||||
import { userDbToTransaction } from '../../convert'
|
|
||||||
import { loadUsers } from '../../database'
|
|
||||||
import { generateKeyPairUserAccount } from '../../keyPair'
|
|
||||||
import { CreatedUserDb } from '../../valibot.schema'
|
|
||||||
import { AbstractSyncRole } from './AbstractSync.role'
|
|
||||||
|
|
||||||
export class UsersSyncRole extends AbstractSyncRole<CreatedUserDb> {
|
|
||||||
getDate(): Date {
|
|
||||||
return this.peek().createdAt
|
|
||||||
}
|
|
||||||
|
|
||||||
itemTypeName(): string {
|
|
||||||
return 'users'
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadFromDb(offset: number, count: number): Promise<CreatedUserDb[]> {
|
|
||||||
const users = await loadUsers(this.context.db, offset, count)
|
|
||||||
for (const user of users) {
|
|
||||||
const communityContext = this.context.getCommunityContextByUuid(user.communityUuid)
|
|
||||||
await generateKeyPairUserAccount(user, this.context.cache, communityContext.topicId)
|
|
||||||
}
|
|
||||||
return users
|
|
||||||
}
|
|
||||||
|
|
||||||
async pushToBlockchain(item: CreatedUserDb): Promise<void> {
|
|
||||||
const communityContext = this.context.getCommunityContextByUuid(item.communityUuid)
|
|
||||||
const transaction = userDbToTransaction(item, communityContext.topicId)
|
|
||||||
return await addRegisterAddressTransaction(communityContext.blockchain, transaction)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,38 +0,0 @@
|
|||||||
import { Profiler } from 'gradido-blockchain-js'
|
|
||||||
import { Context } from '../../Context'
|
|
||||||
import { DeletedTransactionLinksSyncRole } from './DeletedTransactionLinksSync.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 timeUsed = new Profiler()
|
|
||||||
const containers = [
|
|
||||||
new UsersSyncRole(context),
|
|
||||||
new TransactionsSyncRole(context),
|
|
||||||
new DeletedTransactionLinksSyncRole(context),
|
|
||||||
new TransactionLinksSyncRole(context),
|
|
||||||
]
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
timeUsed.reset()
|
|
||||||
const results = await Promise.all(containers.map((c) => c.ensureFilled(batchSize)))
|
|
||||||
const loadedItemsCount = results.reduce((acc, c) => acc + c, 0)
|
|
||||||
// log only, if at least one new item was loaded
|
|
||||||
if (loadedItemsCount && context.logger.isInfoEnabled()) {
|
|
||||||
context.logger.info(`${loadedItemsCount} new items loaded from db in ${timeUsed.string()}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove empty containers
|
|
||||||
const available = containers.filter((c) => !c.isEmpty())
|
|
||||||
if (available.length === 0) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
// sort by date, to ensure container on index 0 is the one with the smallest date
|
|
||||||
if (available.length > 0) {
|
|
||||||
available.sort((a, b) => a.getDate().getTime() - b.getDate().getTime())
|
|
||||||
}
|
|
||||||
await available[0].toBlockchain()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
import { crypto_generichash_batch, crypto_generichash_KEYBYTES } from 'sodium-native'
|
|
||||||
|
|
||||||
export function bytesToMbyte(bytes: number): string {
|
|
||||||
return (bytes / 1024 / 1024).toFixed(4)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function bytesToKbyte(bytes: number): string {
|
|
||||||
return (bytes / 1024).toFixed(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function calculateOneHashStep(hash: Buffer, data: Buffer): Buffer<ArrayBuffer> {
|
|
||||||
const outputHash = Buffer.alloc(crypto_generichash_KEYBYTES, 0)
|
|
||||||
crypto_generichash_batch(outputHash, [hash, data])
|
|
||||||
return outputHash
|
|
||||||
}
|
|
||||||
@ -1,70 +0,0 @@
|
|||||||
import { InMemoryBlockchain } 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 './TransactionTypeId'
|
|
||||||
|
|
||||||
export const createdUserDbSchema = v.object({
|
|
||||||
gradidoId: uuidv4Schema,
|
|
||||||
communityUuid: uuidv4Schema,
|
|
||||||
createdAt: dateSchema,
|
|
||||||
})
|
|
||||||
|
|
||||||
export const userDbSchema = v.object({
|
|
||||||
gradidoId: uuidv4Schema,
|
|
||||||
communityUuid: uuidv4Schema,
|
|
||||||
})
|
|
||||||
|
|
||||||
export const transactionDbSchema = v.object({
|
|
||||||
typeId: v.enum(TransactionTypeId),
|
|
||||||
amount: gradidoAmountSchema,
|
|
||||||
balanceDate: dateSchema,
|
|
||||||
memo: memoSchema,
|
|
||||||
creationDate: v.nullish(dateSchema),
|
|
||||||
user: userDbSchema,
|
|
||||||
linkedUser: userDbSchema,
|
|
||||||
transactionLinkCode: v.nullish(identifierSeedSchema),
|
|
||||||
})
|
|
||||||
|
|
||||||
export const transactionLinkDbSchema = v.object({
|
|
||||||
user: userDbSchema,
|
|
||||||
code: identifierSeedSchema,
|
|
||||||
amount: gradidoAmountSchema,
|
|
||||||
memo: memoSchema,
|
|
||||||
createdAt: dateSchema,
|
|
||||||
validUntil: dateSchema,
|
|
||||||
})
|
|
||||||
|
|
||||||
export const communityDbSchema = v.object({
|
|
||||||
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,
|
|
||||||
folder: v.pipe(
|
|
||||||
v.string(),
|
|
||||||
v.minLength(1, 'expect string length >= 1'),
|
|
||||||
v.maxLength(255, 'expect string length <= 255'),
|
|
||||||
v.regex(/^[a-zA-Z0-9-_]+$/, 'expect string to be a valid (alphanumeric, _, -) folder name'),
|
|
||||||
),
|
|
||||||
})
|
|
||||||
|
|
||||||
export type TransactionDb = v.InferOutput<typeof transactionDbSchema>
|
|
||||||
export type UserDb = v.InferOutput<typeof userDbSchema>
|
|
||||||
export type CreatedUserDb = v.InferOutput<typeof createdUserDbSchema>
|
|
||||||
export type TransactionLinkDb = v.InferOutput<typeof transactionLinkDbSchema>
|
|
||||||
export type CommunityDb = v.InferOutput<typeof communityDbSchema>
|
|
||||||
export type CommunityContext = v.InferOutput<typeof communityContextSchema>
|
|
||||||
@ -1,4 +1,5 @@
|
|||||||
import { heapStats } from 'bun:jsc'
|
import { heapStats } from 'bun:jsc'
|
||||||
|
import dotenv from 'dotenv'
|
||||||
import { drizzle, MySql2Database } from 'drizzle-orm/mysql2'
|
import { drizzle, MySql2Database } from 'drizzle-orm/mysql2'
|
||||||
import { Filter, Profiler, SearchDirection_ASC } from 'gradido-blockchain-js'
|
import { Filter, Profiler, SearchDirection_ASC } from 'gradido-blockchain-js'
|
||||||
import { getLogger, Logger } from 'log4js'
|
import { getLogger, Logger } from 'log4js'
|
||||||
@ -11,6 +12,8 @@ import { Uuidv4 } from '../../schemas/typeGuard.schema'
|
|||||||
import { bytesToMbyte } from './utils'
|
import { bytesToMbyte } from './utils'
|
||||||
import { CommunityContext } from './valibot.schema'
|
import { CommunityContext } from './valibot.schema'
|
||||||
|
|
||||||
|
dotenv.config()
|
||||||
|
|
||||||
export class Context {
|
export class Context {
|
||||||
public logger: Logger
|
public logger: Logger
|
||||||
public db: MySql2Database
|
public db: MySql2Database
|
||||||
@ -36,11 +39,9 @@ export class Context {
|
|||||||
database: CONFIG.MYSQL_DATABASE,
|
database: CONFIG.MYSQL_DATABASE,
|
||||||
port: CONFIG.MYSQL_PORT,
|
port: CONFIG.MYSQL_PORT,
|
||||||
})
|
})
|
||||||
return new Context(
|
const db = drizzle({ client: connection })
|
||||||
getLogger(`${LOG4JS_BASE_CATEGORY}.migrations.db-v2.7.0_to_blockchain-v3.5`),
|
const logger = getLogger(`${LOG4JS_BASE_CATEGORY}.migrations.db-v2.7.0_to_blockchain-v3.5`)
|
||||||
drizzle({ client: connection }),
|
return new Context(logger, db, KeyPairCacheManager.getInstance())
|
||||||
KeyPairCacheManager.getInstance(),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getCommunityContextByUuid(communityUuid: Uuidv4): CommunityContext {
|
getCommunityContextByUuid(communityUuid: Uuidv4): CommunityContext {
|
||||||
@ -9,12 +9,13 @@ import {
|
|||||||
} from 'gradido-blockchain-js'
|
} from 'gradido-blockchain-js'
|
||||||
import { CONFIG } from '../../config'
|
import { CONFIG } from '../../config'
|
||||||
import { Context } from './Context'
|
import { Context } from './Context'
|
||||||
import { bytesToKbyte, calculateOneHashStep } from './utils'
|
import { bytesString, calculateOneHashStep } from './utils'
|
||||||
import { CommunityContext } from './valibot.schema'
|
import { CommunityContext } from './valibot.schema'
|
||||||
|
|
||||||
export function exportAllCommunities(context: Context, batchSize: number) {
|
export function exportAllCommunities(context: Context, batchSize: number) {
|
||||||
const timeUsed = new Profiler()
|
const timeUsed = new Profiler()
|
||||||
for (const communityContext of context.communities.values()) {
|
for (const communityContext of context.communities.values()) {
|
||||||
|
context.logger.info(`exporting community ${communityContext.communityId} to binary file`)
|
||||||
exportCommunity(communityContext, context, batchSize)
|
exportCommunity(communityContext, context, batchSize)
|
||||||
}
|
}
|
||||||
context.logger.info(`time used for exporting communities to binary file: ${timeUsed.string()}`)
|
context.logger.info(`time used for exporting communities to binary file: ${timeUsed.string()}`)
|
||||||
@ -25,36 +26,77 @@ export function exportCommunity(
|
|||||||
context: Context,
|
context: Context,
|
||||||
batchSize: number,
|
batchSize: number,
|
||||||
) {
|
) {
|
||||||
|
const timeUsed = new Profiler()
|
||||||
|
const timeSinceLastPrint = new Profiler()
|
||||||
// write as binary file for GradidoNode
|
// write as binary file for GradidoNode
|
||||||
const f = new Filter()
|
const f = new Filter()
|
||||||
f.pagination.size = batchSize
|
f.pagination.size = batchSize
|
||||||
f.pagination.page = 1
|
f.pagination.page = 1
|
||||||
f.searchDirection = SearchDirection_ASC
|
f.searchDirection = SearchDirection_ASC
|
||||||
const binFilePath = prepareFolder(communityContext)
|
const binFilePath = prepareFolder(communityContext)
|
||||||
|
let count = 0
|
||||||
|
let printCount = 0
|
||||||
|
|
||||||
let lastTransactionCount = 0
|
let lastTransactionCount = 0
|
||||||
|
let triggeredTransactionsCount = 0
|
||||||
let hash = Buffer.alloc(32, 0)
|
let hash = Buffer.alloc(32, 0)
|
||||||
|
const isDebug = context.logger.isDebugEnabled()
|
||||||
|
const printConsole = () => {
|
||||||
|
if (triggeredTransactionsCount > 0) {
|
||||||
|
process.stdout.write(
|
||||||
|
`exported ${count} transactions + ${triggeredTransactionsCount} triggered from timeouted transaction links\r`,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
process.stdout.write(`exported ${count} transactions\r`)
|
||||||
|
}
|
||||||
|
}
|
||||||
do {
|
do {
|
||||||
const transactions = communityContext.blockchain.findAll(f)
|
const transactions = communityContext.blockchain.findAll(f)
|
||||||
lastTransactionCount = transactions.size()
|
lastTransactionCount = transactions.size()
|
||||||
|
|
||||||
for (let i = 0; i < lastTransactionCount; i++) {
|
for (let i = 0; i < lastTransactionCount; i++) {
|
||||||
const confirmedTransaction = transactions.get(i)?.getConfirmedTransaction()
|
const confirmedTransaction = transactions.get(i)?.getConfirmedTransaction()
|
||||||
const transactionNr = f.pagination.page * batchSize + i
|
const transactionNr = (f.pagination.page - 2) * batchSize + i
|
||||||
if (!confirmedTransaction) {
|
if (!confirmedTransaction) {
|
||||||
throw new Error(`invalid TransactionEntry at index: ${transactionNr} `)
|
throw new Error(`invalid TransactionEntry at index: ${transactionNr} `)
|
||||||
}
|
}
|
||||||
hash = exportTransaction(confirmedTransaction, hash, binFilePath)
|
hash = exportTransaction(confirmedTransaction, hash, binFilePath)
|
||||||
|
if (
|
||||||
|
confirmedTransaction
|
||||||
|
?.getGradidoTransaction()
|
||||||
|
?.getTransactionBody()
|
||||||
|
?.isTimeoutDeferredTransfer()
|
||||||
|
) {
|
||||||
|
triggeredTransactionsCount++
|
||||||
|
} else {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
if (isDebug) {
|
||||||
|
if (timeSinceLastPrint.millis() > 100) {
|
||||||
|
printConsole()
|
||||||
|
timeSinceLastPrint.reset()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
printCount++
|
||||||
|
if (printCount >= 100) {
|
||||||
|
printConsole()
|
||||||
|
printCount = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
f.pagination.page++
|
f.pagination.page++
|
||||||
} while (lastTransactionCount === batchSize)
|
} while (lastTransactionCount === batchSize)
|
||||||
|
printConsole()
|
||||||
|
process.stdout.write(`\n`)
|
||||||
|
|
||||||
fs.appendFileSync(binFilePath, hash!)
|
fs.appendFileSync(binFilePath, hash!)
|
||||||
context.logger.info(
|
context.logger.info(
|
||||||
`binary file for community ${communityContext.communityId} written to ${binFilePath}`,
|
`binary file for community ${communityContext.communityId} written to ${binFilePath}`,
|
||||||
)
|
)
|
||||||
|
const sumTransactionsCount = (f.pagination.page - 2) * batchSize + lastTransactionCount
|
||||||
|
const fileSize = fs.statSync(binFilePath).size
|
||||||
context.logger.info(
|
context.logger.info(
|
||||||
`transactions count: ${(f.pagination.page - 1) * batchSize + lastTransactionCount}, size: ${bytesToKbyte(fs.statSync(binFilePath).size)} KByte`,
|
`exported ${sumTransactionsCount} transactions (${bytesString(fileSize)}) in ${timeUsed.string()}`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -0,0 +1,50 @@
|
|||||||
|
import {
|
||||||
|
AccountBalances,
|
||||||
|
Filter,
|
||||||
|
GradidoTransaction,
|
||||||
|
HieroAccountId,
|
||||||
|
InMemoryBlockchain,
|
||||||
|
LedgerAnchor,
|
||||||
|
Profiler,
|
||||||
|
} from 'gradido-blockchain-js'
|
||||||
|
import { NotEnoughGradidoBalanceError } from './errors'
|
||||||
|
|
||||||
|
export const defaultHieroAccount = new HieroAccountId(0, 0, 2)
|
||||||
|
export let callTime: number = 0
|
||||||
|
const timeUsed = new Profiler()
|
||||||
|
|
||||||
|
export function addToBlockchain(
|
||||||
|
transaction: GradidoTransaction,
|
||||||
|
blockchain: InMemoryBlockchain,
|
||||||
|
ledgerAnchor: LedgerAnchor,
|
||||||
|
accountBalances: AccountBalances,
|
||||||
|
): boolean {
|
||||||
|
try {
|
||||||
|
timeUsed.reset()
|
||||||
|
const result = blockchain.createAndAddConfirmedTransactionExternFast(
|
||||||
|
transaction,
|
||||||
|
ledgerAnchor,
|
||||||
|
accountBalances,
|
||||||
|
)
|
||||||
|
callTime += timeUsed.nanos()
|
||||||
|
return result
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
const matches = error.message.match(
|
||||||
|
/not enough Gradido Balance for (send coins|operation), needed: -?(\d+\.\d+), exist: (\d+\.\d+)/,
|
||||||
|
)
|
||||||
|
if (matches) {
|
||||||
|
const needed = parseFloat(matches[2])
|
||||||
|
const exist = parseFloat(matches[3])
|
||||||
|
throw new NotEnoughGradidoBalanceError(needed, exist)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// const wekingheim = InMemoryBlockchainProvider.getInstance().getBlockchain('wekingheim')
|
||||||
|
// const lastTransactionw = wekingheim?.findOne(Filter.LAST_TRANSACTION)
|
||||||
|
|
||||||
|
const lastTransaction = blockchain.findOne(Filter.LAST_TRANSACTION)
|
||||||
|
throw new Error(
|
||||||
|
`Transaction ${transaction.toJson(true)} not added: ${error}, last transaction was: ${lastTransaction?.getConfirmedTransaction()?.toJson(true)}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,105 @@
|
|||||||
|
import { randomBytes } from 'node:crypto'
|
||||||
|
import {
|
||||||
|
AccountBalances,
|
||||||
|
GradidoTransactionBuilder,
|
||||||
|
InMemoryBlockchainProvider,
|
||||||
|
LedgerAnchor,
|
||||||
|
} from 'gradido-blockchain-js'
|
||||||
|
import * as v from 'valibot'
|
||||||
|
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 { toFolderName } from '../../utils/filesystem'
|
||||||
|
import { addToBlockchain } from './blockchain'
|
||||||
|
import { Context } from './Context'
|
||||||
|
import { Balance } from './data/Balance'
|
||||||
|
import {
|
||||||
|
loadAdminUsersCache,
|
||||||
|
loadCommunities,
|
||||||
|
loadContributionLinkModeratorCache,
|
||||||
|
} from './database'
|
||||||
|
import { CommunityContext } from './valibot.schema'
|
||||||
|
|
||||||
|
export async function bootstrap(): Promise<Context> {
|
||||||
|
const context = await Context.create()
|
||||||
|
context.communities = await bootstrapCommunities(context)
|
||||||
|
await Promise.all([
|
||||||
|
loadContributionLinkModeratorCache(context.db),
|
||||||
|
loadAdminUsersCache(context.db),
|
||||||
|
])
|
||||||
|
return context
|
||||||
|
}
|
||||||
|
|
||||||
|
async function bootstrapCommunities(context: Context): Promise<Map<string, CommunityContext>> {
|
||||||
|
const communities = new Map<string, CommunityContext>()
|
||||||
|
const communitiesDb = await loadCommunities(context.db)
|
||||||
|
|
||||||
|
for (const communityDb of communitiesDb) {
|
||||||
|
const communityId = communityDb.communityUuid
|
||||||
|
const blockchain = InMemoryBlockchainProvider.getInstance().getBlockchain(communityId)
|
||||||
|
if (!blockchain) {
|
||||||
|
throw new Error(`Couldn't create Blockchain for community ${communityId}`)
|
||||||
|
}
|
||||||
|
context.logger.info(`Blockchain for community '${communityId}' 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)
|
||||||
|
}
|
||||||
|
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 builder = new GradidoTransactionBuilder()
|
||||||
|
builder
|
||||||
|
.setCreatedAt(creationDate)
|
||||||
|
.setSenderCommunity(communityId)
|
||||||
|
.setCommunityRoot(
|
||||||
|
communityKeyPair.getPublicKey(),
|
||||||
|
gmwKeyPair.getPublicKey(),
|
||||||
|
aufKeyPair.getPublicKey(),
|
||||||
|
)
|
||||||
|
.sign(communityKeyPair)
|
||||||
|
|
||||||
|
const communityContext: CommunityContext = {
|
||||||
|
communityId,
|
||||||
|
foreign: communityDb.foreign,
|
||||||
|
blockchain,
|
||||||
|
keyPair: communityKeyPair,
|
||||||
|
folder: toFolderName(communityId),
|
||||||
|
gmwBalance: new Balance(gmwKeyPair.getPublicKey()!, communityId),
|
||||||
|
aufBalance: new Balance(aufKeyPair.getPublicKey()!, communityId),
|
||||||
|
}
|
||||||
|
communities.set(communityId, communityContext)
|
||||||
|
const accountBalances = new AccountBalances()
|
||||||
|
accountBalances.add(communityContext.aufBalance.getAccountBalance())
|
||||||
|
accountBalances.add(communityContext.gmwBalance.getAccountBalance())
|
||||||
|
addToBlockchain(
|
||||||
|
builder.build(),
|
||||||
|
blockchain,
|
||||||
|
new LedgerAnchor(communityDb.id, LedgerAnchor.Type_LEGACY_GRADIDO_DB_COMMUNITY_ID),
|
||||||
|
accountBalances,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return communities
|
||||||
|
}
|
||||||
@ -0,0 +1,117 @@
|
|||||||
|
import Decimal from 'decimal.js-light'
|
||||||
|
import { AccountBalance, GradidoUnit, MemoryBlockPtr } from 'gradido-blockchain-js'
|
||||||
|
import { NegativeBalanceError } from '../errors'
|
||||||
|
import { legacyCalculateDecay } from '../utils'
|
||||||
|
|
||||||
|
export class Balance {
|
||||||
|
private balance: GradidoUnit
|
||||||
|
private date: Date
|
||||||
|
private publicKey: MemoryBlockPtr
|
||||||
|
private communityId: string
|
||||||
|
|
||||||
|
constructor(publicKey: MemoryBlockPtr, communityId: string) {
|
||||||
|
this.balance = new GradidoUnit(0)
|
||||||
|
this.date = new Date()
|
||||||
|
this.publicKey = publicKey
|
||||||
|
this.communityId = communityId
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromAccountBalance(
|
||||||
|
accountBalance: AccountBalance,
|
||||||
|
confirmedAt: Date,
|
||||||
|
communityId: string,
|
||||||
|
): Balance {
|
||||||
|
const balance = new Balance(accountBalance.getPublicKey()!, communityId)
|
||||||
|
balance.update(accountBalance.getBalance(), confirmedAt)
|
||||||
|
return balance
|
||||||
|
}
|
||||||
|
|
||||||
|
getBalance(): GradidoUnit {
|
||||||
|
return this.balance
|
||||||
|
}
|
||||||
|
|
||||||
|
getDate(): Date {
|
||||||
|
return this.date
|
||||||
|
}
|
||||||
|
|
||||||
|
updateLegacyDecay(amount: GradidoUnit, date: Date) {
|
||||||
|
// make sure to copy instead of referencing
|
||||||
|
const previousBalanceString = this.balance.toString()
|
||||||
|
const previousDate = new Date(this.date.getTime())
|
||||||
|
|
||||||
|
if (this.balance.equal(GradidoUnit.zero())) {
|
||||||
|
this.balance = amount
|
||||||
|
this.date = date
|
||||||
|
} else {
|
||||||
|
const decayedBalance = legacyCalculateDecay(
|
||||||
|
new Decimal(this.balance.toString()),
|
||||||
|
this.date,
|
||||||
|
date,
|
||||||
|
).toDecimalPlaces(4, Decimal.ROUND_CEIL)
|
||||||
|
const newBalance = decayedBalance.add(new Decimal(amount.toString()))
|
||||||
|
this.balance = GradidoUnit.fromString(newBalance.toString())
|
||||||
|
this.date = date
|
||||||
|
}
|
||||||
|
if (this.balance.lt(GradidoUnit.zero())) {
|
||||||
|
if (this.balance.lt(GradidoUnit.fromGradidoCent(100).negated())) {
|
||||||
|
const previousDecayedBalance = legacyCalculateDecay(
|
||||||
|
new Decimal(previousBalanceString),
|
||||||
|
previousDate,
|
||||||
|
date,
|
||||||
|
)
|
||||||
|
throw new NegativeBalanceError(
|
||||||
|
`negative Gradido amount detected in Balance.updateLegacyDecay`,
|
||||||
|
previousBalanceString,
|
||||||
|
amount.toString(),
|
||||||
|
previousDecayedBalance.toString(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
this.balance = GradidoUnit.zero()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
update(amount: GradidoUnit, date: Date) {
|
||||||
|
const previousBalance = new GradidoUnit(this.balance.toString())
|
||||||
|
const previousDate = new Date(this.date.getTime())
|
||||||
|
|
||||||
|
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())) {
|
||||||
|
// ignore diffs less than a gradido cent
|
||||||
|
if (this.balance.lt(GradidoUnit.fromGradidoCent(100).negated())) {
|
||||||
|
const previousDecayedBalance = this.balance.calculateDecay(previousDate, date)
|
||||||
|
throw new NegativeBalanceError(
|
||||||
|
`negative Gradido amount detected in Balance.update`,
|
||||||
|
previousBalance.toString(),
|
||||||
|
amount.toString(),
|
||||||
|
previousDecayedBalance.toString(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
this.balance = GradidoUnit.zero()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getAccountBalance(): AccountBalance {
|
||||||
|
return new AccountBalance(this.publicKey, this.balance, this.communityId)
|
||||||
|
}
|
||||||
|
|
||||||
|
toString(): string {
|
||||||
|
return JSON.stringify(
|
||||||
|
{
|
||||||
|
balance: this.balance.toString(),
|
||||||
|
date: this.date,
|
||||||
|
publicKey: this.publicKey.convertToHex(),
|
||||||
|
communityId: this.communityId,
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
export enum ContributionStatus {
|
||||||
|
PENDING = 'PENDING',
|
||||||
|
DELETED = 'DELETED',
|
||||||
|
IN_PROGRESS = 'IN_PROGRESS',
|
||||||
|
DENIED = 'DENIED',
|
||||||
|
CONFIRMED = 'CONFIRMED',
|
||||||
|
}
|
||||||
@ -1,13 +1,13 @@
|
|||||||
import { KeyPairEd25519, MemoryBlock, MemoryBlockPtr } from 'gradido-blockchain-js'
|
import { KeyPairEd25519, MemoryBlock, MemoryBlockPtr } from 'gradido-blockchain-js'
|
||||||
import { getLogger } from 'log4js'
|
import { getLogger } from 'log4js'
|
||||||
import { KeyPairCacheManager } from '../../cache/KeyPairCacheManager'
|
import { KeyPairCacheManager } from '../../../cache/KeyPairCacheManager'
|
||||||
import { CONFIG } from '../../config'
|
import { CONFIG } from '../../../config'
|
||||||
import { LOG4JS_BASE_CATEGORY } from '../../config/const'
|
import { LOG4JS_BASE_CATEGORY } from '../../../config/const'
|
||||||
import { KeyPairIdentifierLogic } from '../../data/KeyPairIdentifier.logic'
|
import { KeyPairIdentifierLogic } from '../../../data/KeyPairIdentifier.logic'
|
||||||
import { AccountKeyPairRole } from '../../interactions/resolveKeyPair/AccountKeyPair.role'
|
import { AccountKeyPairRole } from '../../../interactions/resolveKeyPair/AccountKeyPair.role'
|
||||||
import { UserKeyPairRole } from '../../interactions/resolveKeyPair/UserKeyPair.role'
|
import { UserKeyPairRole } from '../../../interactions/resolveKeyPair/UserKeyPair.role'
|
||||||
import { HieroId } from '../../schemas/typeGuard.schema'
|
import { HieroId } from '../../../schemas/typeGuard.schema'
|
||||||
import { CommunityDb, UserDb } from './valibot.schema'
|
import { CommunityDb, UserDb } from '../valibot.schema'
|
||||||
|
|
||||||
const logger = getLogger(`${LOG4JS_BASE_CATEGORY}.migrations.db-v2.7.0_to_blockchain-v3.6.keyPair`)
|
const logger = getLogger(`${LOG4JS_BASE_CATEGORY}.migrations.db-v2.7.0_to_blockchain-v3.6.keyPair`)
|
||||||
|
|
||||||
@ -30,7 +30,10 @@ export function generateKeyPairCommunity(
|
|||||||
if (!keyPair) {
|
if (!keyPair) {
|
||||||
throw new Error(`Couldn't create key pair for community ${community.communityUuid}`)
|
throw new Error(`Couldn't create key pair for community ${community.communityUuid}`)
|
||||||
}
|
}
|
||||||
const communityKeyPairKey = new KeyPairIdentifierLogic({ communityTopicId: topicId }).getKey()
|
const communityKeyPairKey = new KeyPairIdentifierLogic({
|
||||||
|
communityTopicId: topicId,
|
||||||
|
communityId: community.communityUuid,
|
||||||
|
}).getKey()
|
||||||
cache.addKeyPair(communityKeyPairKey, keyPair)
|
cache.addKeyPair(communityKeyPairKey, keyPair)
|
||||||
logger.info(`Community Key Pair added with key: ${communityKeyPairKey}`)
|
logger.info(`Community Key Pair added with key: ${communityKeyPairKey}`)
|
||||||
}
|
}
|
||||||
@ -44,6 +47,7 @@ export async function generateKeyPairUserAccount(
|
|||||||
const userKeyPairRole = new UserKeyPairRole(user.gradidoId, communityKeyPair)
|
const userKeyPairRole = new UserKeyPairRole(user.gradidoId, communityKeyPair)
|
||||||
const userKeyPairKey = new KeyPairIdentifierLogic({
|
const userKeyPairKey = new KeyPairIdentifierLogic({
|
||||||
communityTopicId: communityTopicId,
|
communityTopicId: communityTopicId,
|
||||||
|
communityId: user.communityUuid,
|
||||||
account: {
|
account: {
|
||||||
userUuid: user.gradidoId,
|
userUuid: user.gradidoId,
|
||||||
accountNr: 0,
|
accountNr: 0,
|
||||||
@ -56,6 +60,7 @@ export async function generateKeyPairUserAccount(
|
|||||||
const accountKeyPairRole = new AccountKeyPairRole(1, userKeyPair)
|
const accountKeyPairRole = new AccountKeyPairRole(1, userKeyPair)
|
||||||
const accountKeyPairKey = new KeyPairIdentifierLogic({
|
const accountKeyPairKey = new KeyPairIdentifierLogic({
|
||||||
communityTopicId: communityTopicId,
|
communityTopicId: communityTopicId,
|
||||||
|
communityId: user.communityUuid,
|
||||||
account: {
|
account: {
|
||||||
userUuid: user.gradidoId,
|
userUuid: user.gradidoId,
|
||||||
accountNr: 1,
|
accountNr: 1,
|
||||||
@ -0,0 +1,65 @@
|
|||||||
|
import { and, asc, eq, isNotNull, isNull, or, sql } from 'drizzle-orm'
|
||||||
|
import { MySql2Database } from 'drizzle-orm/mysql2'
|
||||||
|
import * as v from 'valibot'
|
||||||
|
import { communitiesTable, eventsTable, userRolesTable, usersTable } from './drizzle.schema'
|
||||||
|
import { CommunityDb, communityDbSchema, UserDb, userDbSchema } from './valibot.schema'
|
||||||
|
|
||||||
|
export const contributionLinkModerators = new Map<number, UserDb>()
|
||||||
|
export const adminUsers = new Map<string, UserDb>()
|
||||||
|
|
||||||
|
export async function loadContributionLinkModeratorCache(db: MySql2Database): Promise<void> {
|
||||||
|
const result = await db
|
||||||
|
.select({
|
||||||
|
event: eventsTable,
|
||||||
|
user: usersTable,
|
||||||
|
})
|
||||||
|
.from(eventsTable)
|
||||||
|
.innerJoin(usersTable, eq(eventsTable.actingUserId, usersTable.id))
|
||||||
|
.where(eq(eventsTable.type, 'ADMIN_CONTRIBUTION_LINK_CREATE'))
|
||||||
|
.orderBy(asc(eventsTable.id))
|
||||||
|
|
||||||
|
result.map((row: any) => {
|
||||||
|
contributionLinkModerators.set(
|
||||||
|
row.event.involvedContributionLinkId,
|
||||||
|
v.parse(userDbSchema, row.user),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadAdminUsersCache(db: MySql2Database): Promise<void> {
|
||||||
|
const result = await db
|
||||||
|
.select({
|
||||||
|
user: usersTable,
|
||||||
|
})
|
||||||
|
.from(userRolesTable)
|
||||||
|
.where(eq(userRolesTable.role, 'ADMIN'))
|
||||||
|
.innerJoin(usersTable, eq(userRolesTable.userId, usersTable.id))
|
||||||
|
|
||||||
|
result.map((row: any) => {
|
||||||
|
adminUsers.set(row.gradidoId, v.parse(userDbSchema, row.user))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// queries
|
||||||
|
export async function loadCommunities(db: MySql2Database): Promise<CommunityDb[]> {
|
||||||
|
const result = await db
|
||||||
|
.select({
|
||||||
|
id: communitiesTable.id,
|
||||||
|
foreign: communitiesTable.foreign,
|
||||||
|
communityUuid: communitiesTable.communityUuid,
|
||||||
|
name: communitiesTable.name,
|
||||||
|
creationDate: communitiesTable.creationDate,
|
||||||
|
userMinCreatedAt: sql`MIN(${usersTable.createdAt})`,
|
||||||
|
})
|
||||||
|
.from(communitiesTable)
|
||||||
|
.innerJoin(usersTable, eq(communitiesTable.communityUuid, usersTable.communityUuid))
|
||||||
|
.where(
|
||||||
|
and(isNotNull(communitiesTable.communityUuid), sql`${usersTable.createdAt} > '2000-01-01'`),
|
||||||
|
)
|
||||||
|
.orderBy(asc(communitiesTable.id))
|
||||||
|
.groupBy(communitiesTable.communityUuid)
|
||||||
|
|
||||||
|
return result.map((row: any) => {
|
||||||
|
return v.parse(communityDbSchema, row)
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -24,12 +24,34 @@ export const communitiesTable = mysqlTable(
|
|||||||
(table) => [unique('uuid_key').on(table.communityUuid)],
|
(table) => [unique('uuid_key').on(table.communityUuid)],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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`),
|
||||||
|
contributionStatus: varchar('contribution_status', { length: 12 }).default("'PENDING'").notNull(),
|
||||||
|
transactionId: int('transaction_id').default(sql`NULL`),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const eventsTable = mysqlTable('events', {
|
||||||
|
id: int().autoincrement().notNull(),
|
||||||
|
type: varchar({ length: 100 }).notNull(),
|
||||||
|
actingUserId: int('acting_user_id').notNull(),
|
||||||
|
involvedContributionLinkId: int('involved_contribution_link_id').default(sql`NULL`),
|
||||||
|
})
|
||||||
|
|
||||||
export const usersTable = mysqlTable(
|
export const usersTable = mysqlTable(
|
||||||
'users',
|
'users',
|
||||||
{
|
{
|
||||||
id: int().autoincrement().notNull(),
|
id: int().autoincrement().notNull(),
|
||||||
|
foreign: tinyint().default(0).notNull(),
|
||||||
gradidoId: char('gradido_id', { length: 36 }).notNull(),
|
gradidoId: char('gradido_id', { length: 36 }).notNull(),
|
||||||
communityUuid: varchar('community_uuid', { length: 36 }).default(sql`NULL`),
|
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 })
|
createdAt: datetime('created_at', { mode: 'string', fsp: 3 })
|
||||||
.default(sql`current_timestamp(3)`)
|
.default(sql`current_timestamp(3)`)
|
||||||
.notNull(),
|
.notNull(),
|
||||||
@ -37,6 +59,16 @@ export const usersTable = mysqlTable(
|
|||||||
(table) => [unique('uuid_key').on(table.gradidoId, table.communityUuid)],
|
(table) => [unique('uuid_key').on(table.gradidoId, table.communityUuid)],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export const userRolesTable = mysqlTable(
|
||||||
|
'user_roles',
|
||||||
|
{
|
||||||
|
id: int().autoincrement().notNull(),
|
||||||
|
userId: int('user_id').notNull(),
|
||||||
|
role: varchar({ length: 40 }).notNull(),
|
||||||
|
},
|
||||||
|
(table) => [index('user_id').on(table.userId)],
|
||||||
|
)
|
||||||
|
|
||||||
export const transactionsTable = mysqlTable(
|
export const transactionsTable = mysqlTable(
|
||||||
'transactions',
|
'transactions',
|
||||||
{
|
{
|
||||||
@ -44,6 +76,7 @@ export const transactionsTable = mysqlTable(
|
|||||||
typeId: int('type_id').default(sql`NULL`),
|
typeId: int('type_id').default(sql`NULL`),
|
||||||
transactionLinkId: int('transaction_link_id').default(sql`NULL`),
|
transactionLinkId: int('transaction_link_id').default(sql`NULL`),
|
||||||
amount: decimal({ precision: 40, scale: 20 }).default(sql`NULL`),
|
amount: decimal({ precision: 40, scale: 20 }).default(sql`NULL`),
|
||||||
|
balance: decimal({ precision: 40, scale: 20 }).default(sql`NULL`),
|
||||||
balanceDate: datetime('balance_date', { mode: 'string', fsp: 3 })
|
balanceDate: datetime('balance_date', { mode: 'string', fsp: 3 })
|
||||||
.default(sql`current_timestamp(3)`)
|
.default(sql`current_timestamp(3)`)
|
||||||
.notNull(),
|
.notNull(),
|
||||||
@ -51,6 +84,9 @@ export const transactionsTable = mysqlTable(
|
|||||||
creationDate: datetime('creation_date', { mode: 'string', fsp: 3 }).default(sql`NULL`),
|
creationDate: datetime('creation_date', { mode: 'string', fsp: 3 }).default(sql`NULL`),
|
||||||
userId: int('user_id').notNull(),
|
userId: int('user_id').notNull(),
|
||||||
linkedUserId: int('linked_user_id').default(sql`NULL`),
|
linkedUserId: int('linked_user_id').default(sql`NULL`),
|
||||||
|
linkedUserCommunityUuid: char('linked_user_community_uuid', { length: 36 }).default(sql`NULL`),
|
||||||
|
linkedUserGradidoId: char('linked_user_gradido_id', { length: 36 }).default(sql`NULL`),
|
||||||
|
linkedTransactionId: int('linked_transaction_id').default(sql`NULL`),
|
||||||
},
|
},
|
||||||
(table) => [index('user_id').on(table.userId)],
|
(table) => [index('user_id').on(table.userId)],
|
||||||
)
|
)
|
||||||
@ -59,9 +95,12 @@ export const transactionLinksTable = mysqlTable('transaction_links', {
|
|||||||
id: int().autoincrement().notNull(),
|
id: int().autoincrement().notNull(),
|
||||||
userId: int().notNull(),
|
userId: int().notNull(),
|
||||||
amount: decimal({ precision: 40, scale: 20 }).notNull(),
|
amount: decimal({ precision: 40, scale: 20 }).notNull(),
|
||||||
|
holdAvailableAmount: decimal('hold_available_amount', { precision: 40, scale: 20 }).notNull(),
|
||||||
memo: varchar({ length: 255 }).notNull(),
|
memo: varchar({ length: 255 }).notNull(),
|
||||||
code: varchar({ length: 24 }).notNull(),
|
code: varchar({ length: 24 }).notNull(),
|
||||||
createdAt: datetime({ mode: 'string' }).notNull(),
|
createdAt: datetime({ mode: 'string' }).notNull(),
|
||||||
deletedAt: datetime({ mode: 'string' }).default(sql`NULL`),
|
deletedAt: datetime({ mode: 'string' }).default(sql`NULL`),
|
||||||
validUntil: datetime({ mode: 'string' }).notNull(),
|
validUntil: datetime({ mode: 'string' }).notNull(),
|
||||||
|
redeemedAt: datetime({ mode: 'string' }).default(sql`NULL`),
|
||||||
|
redeemedBy: int().default(sql`NULL`),
|
||||||
})
|
})
|
||||||
@ -0,0 +1,67 @@
|
|||||||
|
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`,
|
||||||
|
)
|
||||||
|
this.name = 'NotEnoughGradidoBalanceError'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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'
|
||||||
|
this.cause = originalError
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.cause = originalError
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class NegativeBalanceError extends Error {
|
||||||
|
constructor(
|
||||||
|
message: string,
|
||||||
|
previousBalanceString: string,
|
||||||
|
amount: string,
|
||||||
|
previousDecayedBalance: string,
|
||||||
|
) {
|
||||||
|
const parts: string[] = [`NegativeBalanceError in ${message}`]
|
||||||
|
parts.push(`Previous balance: ${previousBalanceString}`)
|
||||||
|
parts.push(`Amount: ${amount}`)
|
||||||
|
parts.push(`Previous decayed balance: ${previousDecayedBalance}`)
|
||||||
|
super(parts.join('\n'))
|
||||||
|
this.name = 'NegativeBalanceError'
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,62 @@
|
|||||||
|
import { Filter, Profiler, ThreadingPolicy_Half, verifySignatures } from 'gradido-blockchain-js'
|
||||||
|
import { onShutdown } from '../../../../shared/src/helper/onShutdown'
|
||||||
|
import { exportAllCommunities } from './binaryExport'
|
||||||
|
import { bootstrap } from './bootstrap'
|
||||||
|
import { syncDbWithBlockchainContext } from './interaction/syncDbWithBlockchain/syncDbWithBlockchain.context'
|
||||||
|
|
||||||
|
// import { hello } from '../../../zig/hello.zig'
|
||||||
|
|
||||||
|
const BATCH_SIZE = 1000
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
// hello()
|
||||||
|
// return
|
||||||
|
// prepare in memory blockchains
|
||||||
|
const context = await bootstrap()
|
||||||
|
onShutdown(async (reason, error) => {
|
||||||
|
context.logger.info(`shutdown reason: ${reason}`)
|
||||||
|
if (error) {
|
||||||
|
context.logger.error(error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// synchronize to in memory blockchain
|
||||||
|
try {
|
||||||
|
await syncDbWithBlockchainContext(context, BATCH_SIZE)
|
||||||
|
} catch (e) {
|
||||||
|
context.logger.error(e)
|
||||||
|
//context.logBlogchain(v.parse(uuidv4Schema, 'e70da33e-5976-4767-bade-aa4e4fa1c01a'))
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeUsed = new Profiler()
|
||||||
|
// bulk verify transaction signatures
|
||||||
|
for (const communityContext of context.communities.values()) {
|
||||||
|
// verifySignatures(Filter.ALL_TRANSACTIONS, ThreadingPolicy_Half)
|
||||||
|
const result = verifySignatures(
|
||||||
|
Filter.ALL_TRANSACTIONS,
|
||||||
|
communityContext.communityId,
|
||||||
|
ThreadingPolicy_Half,
|
||||||
|
)
|
||||||
|
if (!result.isEmpty()) {
|
||||||
|
throw new Error(
|
||||||
|
`Verification of signatures failed for community ${communityContext.communityId}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
context.logger.info(`verified in ${timeUsed.string()}`)
|
||||||
|
|
||||||
|
// write as binary file for GradidoNode
|
||||||
|
exportAllCommunities(context, BATCH_SIZE)
|
||||||
|
|
||||||
|
// log runtime statistics
|
||||||
|
context.logRuntimeStatistics()
|
||||||
|
|
||||||
|
// needed because of shutdown handler (TODO: fix shutdown handler)
|
||||||
|
process.exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((e) => {
|
||||||
|
// biome-ignore lint/suspicious/noConsole: maybe logger isn't initialized here
|
||||||
|
console.error(e)
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
@ -0,0 +1,170 @@
|
|||||||
|
import {
|
||||||
|
AccountBalances,
|
||||||
|
Filter,
|
||||||
|
GradidoTransactionBuilder,
|
||||||
|
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 type IndexType = {
|
||||||
|
date: Date
|
||||||
|
id: number
|
||||||
|
}
|
||||||
|
export let nanosBalanceForUser = 0
|
||||||
|
const lastBalanceOfUserTimeUsed = new Profiler()
|
||||||
|
|
||||||
|
export abstract class AbstractSyncRole<ItemType> {
|
||||||
|
private items: ItemType[] = []
|
||||||
|
protected lastIndex: IndexType = { date: new Date(0), id: 0 }
|
||||||
|
protected logger: Logger
|
||||||
|
protected transactionBuilder: GradidoTransactionBuilder
|
||||||
|
protected accountBalances: AccountBalances
|
||||||
|
|
||||||
|
constructor(protected readonly context: Context) {
|
||||||
|
this.logger = getLogger(
|
||||||
|
`${LOG4JS_BASE_CATEGORY}.migrations.db-v2.7.0_to_blockchain-v3.5.interaction.syncDbWithBlockchain`,
|
||||||
|
)
|
||||||
|
this.transactionBuilder = new GradidoTransactionBuilder()
|
||||||
|
this.accountBalances = new AccountBalances()
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
lastBalanceOfUserTimeUsed.reset()
|
||||||
|
if (publicKey.isEmpty()) {
|
||||||
|
throw new Error('publicKey is empty')
|
||||||
|
}
|
||||||
|
const f = Filter.lastBalanceFor(publicKey)
|
||||||
|
f.setCommunityId(communityId)
|
||||||
|
const lastSenderTransaction = blockchain.findOne(f)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
const result = Balance.fromAccountBalance(
|
||||||
|
senderLastAccountBalance,
|
||||||
|
lastConfirmedTransaction.getConfirmedAt().getDate(),
|
||||||
|
communityId,
|
||||||
|
)
|
||||||
|
nanosBalanceForUser += lastBalanceOfUserTimeUsed.nanos()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
logLastBalanceChangingTransactions(
|
||||||
|
publicKey: MemoryBlockPtr,
|
||||||
|
blockchain: InMemoryBlockchain,
|
||||||
|
transactionCount: number = 5,
|
||||||
|
) {
|
||||||
|
if (!this.context.logger.isDebugEnabled()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const f = new Filter()
|
||||||
|
f.updatedBalancePublicKey = publicKey
|
||||||
|
f.searchDirection = SearchDirection_DESC
|
||||||
|
f.pagination.size = transactionCount
|
||||||
|
const lastTransactions = blockchain.findAll(f)
|
||||||
|
for (let i = lastTransactions.size() - 1; i >= 0; i--) {
|
||||||
|
const tx = lastTransactions.get(i)
|
||||||
|
this.context.logger.debug(`${i}: ${tx?.getConfirmedTransaction()!.toJson(true)}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract getDate(): Date
|
||||||
|
// for using seek rather than offset pagination approach
|
||||||
|
abstract getLastIndex(): IndexType
|
||||||
|
abstract loadFromDb(lastIndex: IndexType, count: number): Promise<ItemType[]>
|
||||||
|
abstract pushToBlockchain(item: ItemType): void
|
||||||
|
abstract itemTypeName(): string
|
||||||
|
abstract getCommunityUuids(): Uuidv4[]
|
||||||
|
|
||||||
|
// return count of new loaded items
|
||||||
|
async ensureFilled(batchSize: number): Promise<number> {
|
||||||
|
if (this.items.length === 0) {
|
||||||
|
let timeUsed: Profiler | undefined
|
||||||
|
if (this.logger.isDebugEnabled()) {
|
||||||
|
timeUsed = new Profiler()
|
||||||
|
}
|
||||||
|
this.items = await this.loadFromDb(this.lastIndex, batchSize)
|
||||||
|
if (this.length > 0) {
|
||||||
|
this.lastIndex = this.getLastIndex()
|
||||||
|
if (timeUsed) {
|
||||||
|
this.logger.debug(
|
||||||
|
`${timeUsed.string()} for loading ${this.items.length} ${this.itemTypeName()} from db`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this.items.length
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
toBlockchain(): void {
|
||||||
|
if (this.isEmpty()) {
|
||||||
|
throw new Error(`[toBlockchain] No items, please call this only if isEmpty returns false`)
|
||||||
|
}
|
||||||
|
this.pushToBlockchain(this.shift())
|
||||||
|
}
|
||||||
|
|
||||||
|
peek(): ItemType {
|
||||||
|
if (this.isEmpty()) {
|
||||||
|
throw new Error(`[peek] No items, please call this only if isEmpty returns false`)
|
||||||
|
}
|
||||||
|
return this.items[0]
|
||||||
|
}
|
||||||
|
peekLast(): ItemType {
|
||||||
|
if (this.isEmpty()) {
|
||||||
|
throw new Error(`[peekLast] No items, please call this only if isEmpty returns false`)
|
||||||
|
}
|
||||||
|
return this.items[this.items.length - 1]
|
||||||
|
}
|
||||||
|
|
||||||
|
shift(): ItemType {
|
||||||
|
const item = this.items.shift()
|
||||||
|
if (!item) {
|
||||||
|
throw new Error(`[shift] No items, shift return undefined`)
|
||||||
|
}
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
|
||||||
|
get length(): number {
|
||||||
|
return this.items.length
|
||||||
|
}
|
||||||
|
|
||||||
|
isEmpty(): boolean {
|
||||||
|
return this.items.length === 0
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,69 @@
|
|||||||
|
import { and, asc, eq, gt, isNotNull, or } from 'drizzle-orm'
|
||||||
|
import * as v from 'valibot'
|
||||||
|
import { Context } from '../../Context'
|
||||||
|
import { ContributionStatus } from '../../data/ContributionStatus'
|
||||||
|
import { contributionLinkModerators } from '../../database'
|
||||||
|
import { contributionsTable, usersTable } from '../../drizzle.schema'
|
||||||
|
import { DatabaseError } from '../../errors'
|
||||||
|
import { toMysqlDateTime } from '../../utils'
|
||||||
|
import { CreationTransactionDb, creationTransactionDbSchema } from '../../valibot.schema'
|
||||||
|
import { IndexType } from './AbstractSync.role'
|
||||||
|
import { CreationsSyncRole } from './CreationsSync.role'
|
||||||
|
|
||||||
|
export class ContributionLinkTransactionSyncRole extends CreationsSyncRole {
|
||||||
|
constructor(readonly context: Context) {
|
||||||
|
super(context)
|
||||||
|
}
|
||||||
|
itemTypeName(): string {
|
||||||
|
return 'contributionLinkTransaction'
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadFromDb(lastIndex: IndexType, count: number): Promise<CreationTransactionDb[]> {
|
||||||
|
const result = await this.context.db
|
||||||
|
.select({
|
||||||
|
contribution: contributionsTable,
|
||||||
|
user: usersTable,
|
||||||
|
})
|
||||||
|
.from(contributionsTable)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
isNotNull(contributionsTable.contributionLinkId),
|
||||||
|
eq(contributionsTable.contributionStatus, ContributionStatus.CONFIRMED),
|
||||||
|
or(
|
||||||
|
gt(contributionsTable.confirmedAt, toMysqlDateTime(lastIndex.date)),
|
||||||
|
and(
|
||||||
|
eq(contributionsTable.confirmedAt, toMysqlDateTime(lastIndex.date)),
|
||||||
|
gt(contributionsTable.transactionId, lastIndex.id),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.innerJoin(usersTable, eq(contributionsTable.userId, usersTable.id))
|
||||||
|
.orderBy(asc(contributionsTable.confirmedAt), asc(contributionsTable.transactionId))
|
||||||
|
.limit(count)
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,181 @@
|
|||||||
|
import { and, asc, eq, gt, isNull, or } from 'drizzle-orm'
|
||||||
|
import { alias } from 'drizzle-orm/mysql-core'
|
||||||
|
import {
|
||||||
|
AccountBalances,
|
||||||
|
AuthenticatedEncryption,
|
||||||
|
EncryptedMemo,
|
||||||
|
Filter,
|
||||||
|
GradidoTransactionBuilder,
|
||||||
|
KeyPairEd25519,
|
||||||
|
LedgerAnchor,
|
||||||
|
MemoryBlockPtr,
|
||||||
|
SearchDirection_DESC,
|
||||||
|
TransactionType_CREATION,
|
||||||
|
TransferAmount,
|
||||||
|
} from 'gradido-blockchain-js'
|
||||||
|
import * as v from 'valibot'
|
||||||
|
import { Uuidv4 } from '../../../../schemas/typeGuard.schema'
|
||||||
|
import { addToBlockchain } from '../../blockchain'
|
||||||
|
import { Context } from '../../Context'
|
||||||
|
import { ContributionStatus } from '../../data/ContributionStatus'
|
||||||
|
import { contributionsTable, usersTable } from '../../drizzle.schema'
|
||||||
|
import { BlockchainError, DatabaseError } from '../../errors'
|
||||||
|
import { toMysqlDateTime } from '../../utils'
|
||||||
|
import {
|
||||||
|
CommunityContext,
|
||||||
|
CreationTransactionDb,
|
||||||
|
creationTransactionDbSchema,
|
||||||
|
} from '../../valibot.schema'
|
||||||
|
import { AbstractSyncRole, IndexType } from './AbstractSync.role'
|
||||||
|
|
||||||
|
export class CreationsSyncRole extends AbstractSyncRole<CreationTransactionDb> {
|
||||||
|
constructor(context: Context) {
|
||||||
|
super(context)
|
||||||
|
this.accountBalances.reserve(3)
|
||||||
|
}
|
||||||
|
|
||||||
|
getDate(): Date {
|
||||||
|
return this.peek().confirmedAt
|
||||||
|
}
|
||||||
|
getCommunityUuids(): Uuidv4[] {
|
||||||
|
return [this.peek().user.communityUuid]
|
||||||
|
}
|
||||||
|
|
||||||
|
getLastIndex(): IndexType {
|
||||||
|
const lastItem = this.peekLast()
|
||||||
|
return { date: lastItem.confirmedAt, id: lastItem.transactionId }
|
||||||
|
}
|
||||||
|
|
||||||
|
itemTypeName(): string {
|
||||||
|
return 'creationTransactions'
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadFromDb(lastIndex: IndexType, count: number): Promise<CreationTransactionDb[]> {
|
||||||
|
const confirmedByUsers = alias(usersTable, 'confirmedByUser')
|
||||||
|
const result = await this.context.db
|
||||||
|
.select({
|
||||||
|
contribution: contributionsTable,
|
||||||
|
user: usersTable,
|
||||||
|
confirmedByUser: confirmedByUsers,
|
||||||
|
})
|
||||||
|
.from(contributionsTable)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
isNull(contributionsTable.contributionLinkId),
|
||||||
|
eq(contributionsTable.contributionStatus, ContributionStatus.CONFIRMED),
|
||||||
|
or(
|
||||||
|
gt(contributionsTable.confirmedAt, toMysqlDateTime(lastIndex.date)),
|
||||||
|
and(
|
||||||
|
eq(contributionsTable.confirmedAt, toMysqlDateTime(lastIndex.date)),
|
||||||
|
gt(contributionsTable.transactionId, lastIndex.id),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.innerJoin(usersTable, eq(contributionsTable.userId, usersTable.id))
|
||||||
|
.innerJoin(confirmedByUsers, eq(contributionsTable.confirmedBy, confirmedByUsers.id))
|
||||||
|
.orderBy(asc(contributionsTable.confirmedAt), asc(contributionsTable.transactionId))
|
||||||
|
.limit(count)
|
||||||
|
|
||||||
|
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 this.transactionBuilder
|
||||||
|
.setCreatedAt(item.confirmedAt)
|
||||||
|
.setRecipientCommunity(communityContext.communityId)
|
||||||
|
.addMemo(
|
||||||
|
new EncryptedMemo(
|
||||||
|
item.memo,
|
||||||
|
new AuthenticatedEncryption(communityContext.keyPair),
|
||||||
|
new AuthenticatedEncryption(recipientKeyPair),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.setTransactionCreation(
|
||||||
|
new TransferAmount(
|
||||||
|
recipientKeyPair.getPublicKey(),
|
||||||
|
item.amount,
|
||||||
|
communityContext.communityId,
|
||||||
|
),
|
||||||
|
item.contributionDate,
|
||||||
|
)
|
||||||
|
.sign(signerKeyPair)
|
||||||
|
}
|
||||||
|
|
||||||
|
calculateAccountBalances(
|
||||||
|
item: CreationTransactionDb,
|
||||||
|
communityContext: CommunityContext,
|
||||||
|
recipientPublicKey: MemoryBlockPtr,
|
||||||
|
): AccountBalances {
|
||||||
|
this.accountBalances.clear()
|
||||||
|
const balance = this.getLastBalanceForUser(
|
||||||
|
recipientPublicKey,
|
||||||
|
communityContext.blockchain,
|
||||||
|
communityContext.communityId,
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
|
||||||
|
this.accountBalances.add(balance.getAccountBalance())
|
||||||
|
this.accountBalances.add(communityContext.aufBalance.getAccountBalance())
|
||||||
|
this.accountBalances.add(communityContext.gmwBalance.getAccountBalance())
|
||||||
|
return this.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).build(),
|
||||||
|
blockchain,
|
||||||
|
new LedgerAnchor(item.id, LedgerAnchor.Type_LEGACY_GRADIDO_DB_CONTRIBUTION_ID),
|
||||||
|
this.calculateAccountBalances(item, communityContext, recipientPublicKey),
|
||||||
|
)
|
||||||
|
} catch (e) {
|
||||||
|
const f = new Filter()
|
||||||
|
f.transactionType = TransactionType_CREATION
|
||||||
|
f.searchDirection = SearchDirection_DESC
|
||||||
|
f.pagination.size = 1
|
||||||
|
const lastContribution = blockchain.findOne(f)
|
||||||
|
if (lastContribution) {
|
||||||
|
this.context.logger.warn(
|
||||||
|
`last contribution: ${lastContribution.getConfirmedTransaction()?.toJson(true)}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
throw new BlockchainError(`Error adding ${this.itemTypeName()}`, item, e as Error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,205 @@
|
|||||||
|
import { and, asc, eq, gt, isNotNull, lt, or } from 'drizzle-orm'
|
||||||
|
import {
|
||||||
|
AccountBalance,
|
||||||
|
AccountBalances,
|
||||||
|
Filter,
|
||||||
|
GradidoDeferredTransfer,
|
||||||
|
GradidoTransactionBuilder,
|
||||||
|
GradidoTransfer,
|
||||||
|
GradidoUnit,
|
||||||
|
KeyPairEd25519,
|
||||||
|
LedgerAnchor,
|
||||||
|
MemoryBlockPtr,
|
||||||
|
TransferAmount,
|
||||||
|
} from 'gradido-blockchain-js'
|
||||||
|
import * as v from 'valibot'
|
||||||
|
import { deriveFromCode } from '../../../../data/deriveKeyPair'
|
||||||
|
import { Uuidv4 } from '../../../../schemas/typeGuard.schema'
|
||||||
|
import { addToBlockchain } from '../../blockchain'
|
||||||
|
import { Context } from '../../Context'
|
||||||
|
import { Balance } from '../../data/Balance'
|
||||||
|
import { transactionLinksTable, usersTable } from '../../drizzle.schema'
|
||||||
|
import { BlockchainError, DatabaseError } from '../../errors'
|
||||||
|
import { toMysqlDateTime } from '../../utils'
|
||||||
|
import {
|
||||||
|
CommunityContext,
|
||||||
|
DeletedTransactionLinkDb,
|
||||||
|
deletedTransactionLinKDbSchema,
|
||||||
|
} from '../../valibot.schema'
|
||||||
|
import { AbstractSyncRole, IndexType } from './AbstractSync.role'
|
||||||
|
|
||||||
|
export class DeletedTransactionLinksSyncRole extends AbstractSyncRole<DeletedTransactionLinkDb> {
|
||||||
|
constructor(context: Context) {
|
||||||
|
super(context)
|
||||||
|
this.accountBalances.reserve(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
getDate(): Date {
|
||||||
|
return this.peek().deletedAt
|
||||||
|
}
|
||||||
|
getCommunityUuids(): Uuidv4[] {
|
||||||
|
return [this.peek().user.communityUuid]
|
||||||
|
}
|
||||||
|
|
||||||
|
getLastIndex(): IndexType {
|
||||||
|
const lastItem = this.peekLast()
|
||||||
|
return { date: lastItem.deletedAt, id: lastItem.id }
|
||||||
|
}
|
||||||
|
|
||||||
|
itemTypeName(): string {
|
||||||
|
return 'deletedTransactionLinks'
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadFromDb(lastIndex: IndexType, count: number): Promise<DeletedTransactionLinkDb[]> {
|
||||||
|
const result = await this.context.db
|
||||||
|
.select({
|
||||||
|
transactionLink: transactionLinksTable,
|
||||||
|
user: usersTable,
|
||||||
|
})
|
||||||
|
.from(transactionLinksTable)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
isNotNull(transactionLinksTable.deletedAt),
|
||||||
|
lt(transactionLinksTable.deletedAt, transactionLinksTable.validUntil),
|
||||||
|
or(
|
||||||
|
gt(transactionLinksTable.deletedAt, toMysqlDateTime(lastIndex.date)),
|
||||||
|
and(
|
||||||
|
eq(transactionLinksTable.deletedAt, toMysqlDateTime(lastIndex.date)),
|
||||||
|
gt(transactionLinksTable.id, lastIndex.id),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.innerJoin(usersTable, eq(transactionLinksTable.userId, usersTable.id))
|
||||||
|
.orderBy(asc(transactionLinksTable.deletedAt), asc(transactionLinksTable.id))
|
||||||
|
.limit(count)
|
||||||
|
|
||||||
|
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(
|
||||||
|
communityContext: CommunityContext,
|
||||||
|
item: DeletedTransactionLinkDb,
|
||||||
|
linkFundingTransactionNr: number,
|
||||||
|
restAmount: GradidoUnit,
|
||||||
|
senderKeyPair: KeyPairEd25519,
|
||||||
|
linkFundingPublicKey: MemoryBlockPtr,
|
||||||
|
): GradidoTransactionBuilder {
|
||||||
|
return this.transactionBuilder
|
||||||
|
.setCreatedAt(item.deletedAt)
|
||||||
|
.setSenderCommunity(communityContext.communityId)
|
||||||
|
.setRedeemDeferredTransfer(
|
||||||
|
linkFundingTransactionNr,
|
||||||
|
new GradidoTransfer(
|
||||||
|
new TransferAmount(
|
||||||
|
senderKeyPair.getPublicKey(),
|
||||||
|
restAmount,
|
||||||
|
communityContext.communityId,
|
||||||
|
),
|
||||||
|
linkFundingPublicKey,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.sign(senderKeyPair)
|
||||||
|
}
|
||||||
|
|
||||||
|
calculateBalances(
|
||||||
|
item: DeletedTransactionLinkDb,
|
||||||
|
fundingTransaction: GradidoDeferredTransfer,
|
||||||
|
senderLastBalance: Balance,
|
||||||
|
communityContext: CommunityContext,
|
||||||
|
senderPublicKey: MemoryBlockPtr,
|
||||||
|
): AccountBalances {
|
||||||
|
this.accountBalances.clear()
|
||||||
|
|
||||||
|
const fundingUserLastBalance = this.getLastBalanceForUser(
|
||||||
|
fundingTransaction.getSenderPublicKey()!,
|
||||||
|
communityContext.blockchain,
|
||||||
|
communityContext.communityId,
|
||||||
|
)
|
||||||
|
fundingUserLastBalance.updateLegacyDecay(senderLastBalance.getBalance(), item.deletedAt)
|
||||||
|
|
||||||
|
// account of link is set to zero, gdd will be send back to initiator
|
||||||
|
this.accountBalances.add(
|
||||||
|
new AccountBalance(senderPublicKey, GradidoUnit.zero(), communityContext.communityId),
|
||||||
|
)
|
||||||
|
this.accountBalances.add(fundingUserLastBalance.getAccountBalance())
|
||||||
|
return this.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,
|
||||||
|
communityContext.communityId,
|
||||||
|
)
|
||||||
|
senderLastBalance.updateLegacyDecay(GradidoUnit.zero(), item.deletedAt)
|
||||||
|
|
||||||
|
try {
|
||||||
|
addToBlockchain(
|
||||||
|
this.buildTransaction(
|
||||||
|
communityContext,
|
||||||
|
item,
|
||||||
|
transaction.getTransactionNr(),
|
||||||
|
senderLastBalance.getBalance(),
|
||||||
|
senderKeyPair,
|
||||||
|
linkFundingPublicKey,
|
||||||
|
).build(),
|
||||||
|
blockchain,
|
||||||
|
new LedgerAnchor(item.id, LedgerAnchor.Type_LEGACY_GRADIDO_DB_TRANSACTION_LINK_ID),
|
||||||
|
this.calculateBalances(
|
||||||
|
item,
|
||||||
|
deferredTransfer,
|
||||||
|
senderLastBalance,
|
||||||
|
communityContext,
|
||||||
|
senderPublicKey,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
} catch (e) {
|
||||||
|
throw new BlockchainError(`Error adding ${this.itemTypeName()}`, item, e as Error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,186 @@
|
|||||||
|
import { and, asc, eq, gt, isNotNull, isNull, or } from 'drizzle-orm'
|
||||||
|
import { alias } from 'drizzle-orm/mysql-core'
|
||||||
|
import {
|
||||||
|
AccountBalances,
|
||||||
|
AuthenticatedEncryption,
|
||||||
|
EncryptedMemo,
|
||||||
|
GradidoTransactionBuilder,
|
||||||
|
KeyPairEd25519,
|
||||||
|
LedgerAnchor,
|
||||||
|
MemoryBlockPtr,
|
||||||
|
TransferAmount,
|
||||||
|
} from 'gradido-blockchain-js'
|
||||||
|
import * as v from 'valibot'
|
||||||
|
import { Uuidv4 } from '../../../../schemas/typeGuard.schema'
|
||||||
|
import { addToBlockchain } from '../../blockchain'
|
||||||
|
import { Context } from '../../Context'
|
||||||
|
import { TransactionTypeId } from '../../data/TransactionTypeId'
|
||||||
|
import { transactionsTable, usersTable } from '../../drizzle.schema'
|
||||||
|
import {
|
||||||
|
BlockchainError,
|
||||||
|
DatabaseError,
|
||||||
|
NegativeBalanceError,
|
||||||
|
NotEnoughGradidoBalanceError,
|
||||||
|
} from '../../errors'
|
||||||
|
import { toMysqlDateTime } from '../../utils'
|
||||||
|
import { CommunityContext, TransactionDb, transactionDbSchema } from '../../valibot.schema'
|
||||||
|
import { AbstractSyncRole, IndexType } from './AbstractSync.role'
|
||||||
|
|
||||||
|
export class LocalTransactionsSyncRole extends AbstractSyncRole<TransactionDb> {
|
||||||
|
constructor(context: Context) {
|
||||||
|
super(context)
|
||||||
|
this.accountBalances.reserve(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
getDate(): Date {
|
||||||
|
return this.peek().balanceDate
|
||||||
|
}
|
||||||
|
|
||||||
|
getCommunityUuids(): Uuidv4[] {
|
||||||
|
return [this.peek().user.communityUuid]
|
||||||
|
}
|
||||||
|
|
||||||
|
getLastIndex(): IndexType {
|
||||||
|
const lastItem = this.peekLast()
|
||||||
|
return { date: lastItem.balanceDate, id: lastItem.id }
|
||||||
|
}
|
||||||
|
|
||||||
|
itemTypeName(): string {
|
||||||
|
return 'localTransactions'
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadFromDb(lastIndex: IndexType, count: number): Promise<TransactionDb[]> {
|
||||||
|
const linkedUsers = alias(usersTable, 'linkedUser')
|
||||||
|
const result = await this.context.db
|
||||||
|
.select({
|
||||||
|
transaction: transactionsTable,
|
||||||
|
user: usersTable,
|
||||||
|
linkedUser: linkedUsers,
|
||||||
|
})
|
||||||
|
.from(transactionsTable)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(transactionsTable.typeId, TransactionTypeId.RECEIVE),
|
||||||
|
isNull(transactionsTable.transactionLinkId),
|
||||||
|
isNotNull(transactionsTable.linkedUserId),
|
||||||
|
eq(usersTable.foreign, 0),
|
||||||
|
eq(linkedUsers.foreign, 0),
|
||||||
|
or(
|
||||||
|
gt(transactionsTable.balanceDate, toMysqlDateTime(lastIndex.date)),
|
||||||
|
and(
|
||||||
|
eq(transactionsTable.balanceDate, toMysqlDateTime(lastIndex.date)),
|
||||||
|
gt(transactionsTable.id, lastIndex.id),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.innerJoin(usersTable, eq(transactionsTable.userId, usersTable.id))
|
||||||
|
.innerJoin(linkedUsers, eq(transactionsTable.linkedUserId, linkedUsers.id))
|
||||||
|
.orderBy(asc(transactionsTable.balanceDate), asc(transactionsTable.id))
|
||||||
|
.limit(count)
|
||||||
|
|
||||||
|
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(
|
||||||
|
communityContext: CommunityContext,
|
||||||
|
item: TransactionDb,
|
||||||
|
senderKeyPair: KeyPairEd25519,
|
||||||
|
recipientKeyPair: KeyPairEd25519,
|
||||||
|
): GradidoTransactionBuilder {
|
||||||
|
return this.transactionBuilder
|
||||||
|
.setCreatedAt(item.balanceDate)
|
||||||
|
.addMemo(
|
||||||
|
new EncryptedMemo(
|
||||||
|
item.memo,
|
||||||
|
new AuthenticatedEncryption(senderKeyPair),
|
||||||
|
new AuthenticatedEncryption(recipientKeyPair),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.setSenderCommunity(communityContext.communityId)
|
||||||
|
.setTransactionTransfer(
|
||||||
|
new TransferAmount(senderKeyPair.getPublicKey(), item.amount, communityContext.communityId),
|
||||||
|
recipientKeyPair.getPublicKey(),
|
||||||
|
)
|
||||||
|
.sign(senderKeyPair)
|
||||||
|
}
|
||||||
|
|
||||||
|
calculateBalances(
|
||||||
|
item: TransactionDb,
|
||||||
|
communityContext: CommunityContext,
|
||||||
|
senderPublicKey: MemoryBlockPtr,
|
||||||
|
recipientPublicKey: MemoryBlockPtr,
|
||||||
|
): AccountBalances {
|
||||||
|
this.accountBalances.clear()
|
||||||
|
|
||||||
|
const senderLastBalance = this.getLastBalanceForUser(
|
||||||
|
senderPublicKey,
|
||||||
|
communityContext.blockchain,
|
||||||
|
communityContext.communityId,
|
||||||
|
)
|
||||||
|
const recipientLastBalance = this.getLastBalanceForUser(
|
||||||
|
recipientPublicKey,
|
||||||
|
communityContext.blockchain,
|
||||||
|
communityContext.communityId,
|
||||||
|
)
|
||||||
|
|
||||||
|
try {
|
||||||
|
senderLastBalance.updateLegacyDecay(item.amount.negated(), item.balanceDate)
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof NegativeBalanceError) {
|
||||||
|
this.logLastBalanceChangingTransactions(senderPublicKey, communityContext.blockchain)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
recipientLastBalance.updateLegacyDecay(item.amount, item.balanceDate)
|
||||||
|
|
||||||
|
this.accountBalances.add(senderLastBalance.getAccountBalance())
|
||||||
|
this.accountBalances.add(recipientLastBalance.getAccountBalance())
|
||||||
|
return this.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(communityContext, item, senderKeyPair, recipientKeyPair).build(),
|
||||||
|
blockchain,
|
||||||
|
new LedgerAnchor(item.id, LedgerAnchor.Type_LEGACY_GRADIDO_DB_TRANSACTION_ID),
|
||||||
|
this.calculateBalances(item, communityContext, senderPublicKey, recipientPublicKey),
|
||||||
|
)
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof NotEnoughGradidoBalanceError) {
|
||||||
|
this.logLastBalanceChangingTransactions(senderPublicKey, blockchain)
|
||||||
|
}
|
||||||
|
throw new BlockchainError(`Error adding ${this.itemTypeName()}`, item, e as Error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,230 @@
|
|||||||
|
import { and, asc, eq, gt, isNotNull, isNull, or } from 'drizzle-orm'
|
||||||
|
import { alias } from 'drizzle-orm/mysql-core'
|
||||||
|
import {
|
||||||
|
AccountBalance,
|
||||||
|
AccountBalances,
|
||||||
|
AuthenticatedEncryption,
|
||||||
|
EncryptedMemo,
|
||||||
|
Filter,
|
||||||
|
GradidoDeferredTransfer,
|
||||||
|
GradidoTransactionBuilder,
|
||||||
|
GradidoTransfer,
|
||||||
|
GradidoUnit,
|
||||||
|
KeyPairEd25519,
|
||||||
|
LedgerAnchor,
|
||||||
|
MemoryBlockPtr,
|
||||||
|
TransferAmount,
|
||||||
|
} from 'gradido-blockchain-js'
|
||||||
|
import * as v from 'valibot'
|
||||||
|
import { deriveFromCode } from '../../../../data/deriveKeyPair'
|
||||||
|
import { Uuidv4 } from '../../../../schemas/typeGuard.schema'
|
||||||
|
import { addToBlockchain } from '../../blockchain'
|
||||||
|
import { Context } from '../../Context'
|
||||||
|
import { transactionLinksTable, usersTable } from '../../drizzle.schema'
|
||||||
|
import { BlockchainError, DatabaseError } from '../../errors'
|
||||||
|
import { toMysqlDateTime } from '../../utils'
|
||||||
|
import {
|
||||||
|
CommunityContext,
|
||||||
|
RedeemedTransactionLinkDb,
|
||||||
|
redeemedTransactionLinkDbSchema,
|
||||||
|
} from '../../valibot.schema'
|
||||||
|
import { AbstractSyncRole, IndexType } from './AbstractSync.role'
|
||||||
|
|
||||||
|
export class RedeemTransactionLinksSyncRole extends AbstractSyncRole<RedeemedTransactionLinkDb> {
|
||||||
|
constructor(context: Context) {
|
||||||
|
super(context)
|
||||||
|
this.accountBalances.reserve(3)
|
||||||
|
}
|
||||||
|
|
||||||
|
getDate(): Date {
|
||||||
|
return this.peek().redeemedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
getCommunityUuids(): Uuidv4[] {
|
||||||
|
return [this.peek().user.communityUuid]
|
||||||
|
}
|
||||||
|
|
||||||
|
getLastIndex(): IndexType {
|
||||||
|
const lastItem = this.peekLast()
|
||||||
|
return { date: lastItem.redeemedAt, id: lastItem.id }
|
||||||
|
}
|
||||||
|
|
||||||
|
itemTypeName(): string {
|
||||||
|
return 'redeemTransactionLinks'
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadFromDb(lastIndex: IndexType, count: number): Promise<RedeemedTransactionLinkDb[]> {
|
||||||
|
const redeemedByUser = alias(usersTable, 'redeemedByUser')
|
||||||
|
const result = await this.context.db
|
||||||
|
.select({
|
||||||
|
transactionLink: transactionLinksTable,
|
||||||
|
user: usersTable,
|
||||||
|
redeemedBy: redeemedByUser,
|
||||||
|
})
|
||||||
|
.from(transactionLinksTable)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
isNull(transactionLinksTable.deletedAt),
|
||||||
|
isNotNull(transactionLinksTable.redeemedAt),
|
||||||
|
eq(usersTable.foreign, 0),
|
||||||
|
eq(redeemedByUser.foreign, 0),
|
||||||
|
or(
|
||||||
|
gt(transactionLinksTable.redeemedAt, toMysqlDateTime(lastIndex.date)),
|
||||||
|
and(
|
||||||
|
eq(transactionLinksTable.redeemedAt, toMysqlDateTime(lastIndex.date)),
|
||||||
|
gt(transactionLinksTable.id, lastIndex.id),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.innerJoin(usersTable, eq(transactionLinksTable.userId, usersTable.id))
|
||||||
|
.innerJoin(redeemedByUser, eq(transactionLinksTable.redeemedBy, redeemedByUser.id))
|
||||||
|
.orderBy(asc(transactionLinksTable.redeemedAt), asc(transactionLinksTable.id))
|
||||||
|
.limit(count)
|
||||||
|
|
||||||
|
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(
|
||||||
|
communityContext: CommunityContext,
|
||||||
|
item: RedeemedTransactionLinkDb,
|
||||||
|
linkFundingTransactionNr: number,
|
||||||
|
senderKeyPair: KeyPairEd25519,
|
||||||
|
recipientKeyPair: KeyPairEd25519,
|
||||||
|
): GradidoTransactionBuilder {
|
||||||
|
return this.transactionBuilder
|
||||||
|
.setCreatedAt(item.redeemedAt)
|
||||||
|
.addMemo(
|
||||||
|
new EncryptedMemo(
|
||||||
|
item.memo,
|
||||||
|
new AuthenticatedEncryption(senderKeyPair),
|
||||||
|
new AuthenticatedEncryption(recipientKeyPair),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.setSenderCommunity(communityContext.communityId)
|
||||||
|
.setRedeemDeferredTransfer(
|
||||||
|
linkFundingTransactionNr,
|
||||||
|
new GradidoTransfer(
|
||||||
|
new TransferAmount(
|
||||||
|
senderKeyPair.getPublicKey(),
|
||||||
|
item.amount,
|
||||||
|
communityContext.communityId,
|
||||||
|
),
|
||||||
|
recipientKeyPair.getPublicKey(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.sign(senderKeyPair)
|
||||||
|
}
|
||||||
|
|
||||||
|
calculateBalances(
|
||||||
|
item: RedeemedTransactionLinkDb,
|
||||||
|
fundingTransaction: GradidoDeferredTransfer,
|
||||||
|
communityContext: CommunityContext,
|
||||||
|
senderPublicKey: MemoryBlockPtr,
|
||||||
|
recipientPublicKey: MemoryBlockPtr,
|
||||||
|
): AccountBalances {
|
||||||
|
this.accountBalances.clear()
|
||||||
|
|
||||||
|
const senderLastBalance = this.getLastBalanceForUser(
|
||||||
|
senderPublicKey,
|
||||||
|
communityContext.blockchain,
|
||||||
|
communityContext.communityId,
|
||||||
|
)
|
||||||
|
const fundingUserLastBalance = this.getLastBalanceForUser(
|
||||||
|
fundingTransaction.getSenderPublicKey()!,
|
||||||
|
communityContext.blockchain,
|
||||||
|
communityContext.communityId,
|
||||||
|
)
|
||||||
|
const recipientLastBalance = this.getLastBalanceForUser(
|
||||||
|
recipientPublicKey,
|
||||||
|
communityContext.blockchain,
|
||||||
|
communityContext.communityId,
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
this.accountBalances.add(
|
||||||
|
new AccountBalance(senderPublicKey, GradidoUnit.zero(), communityContext.communityId),
|
||||||
|
)
|
||||||
|
this.accountBalances.add(recipientLastBalance.getAccountBalance())
|
||||||
|
this.accountBalances.add(fundingUserLastBalance.getAccountBalance())
|
||||||
|
return this.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(
|
||||||
|
communityContext,
|
||||||
|
item,
|
||||||
|
transaction.getTransactionNr(),
|
||||||
|
senderKeyPair,
|
||||||
|
recipientKeyPair,
|
||||||
|
).build(),
|
||||||
|
blockchain,
|
||||||
|
new LedgerAnchor(item.id, LedgerAnchor.Type_LEGACY_GRADIDO_DB_TRANSACTION_LINK_ID),
|
||||||
|
this.calculateBalances(
|
||||||
|
item,
|
||||||
|
deferredTransfer,
|
||||||
|
communityContext,
|
||||||
|
senderPublicKey,
|
||||||
|
recipientPublicKey,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
} catch (e) {
|
||||||
|
throw new BlockchainError(`Error adding ${this.itemTypeName()}`, item, e as Error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,266 @@
|
|||||||
|
import { Decimal } from 'decimal.js-light'
|
||||||
|
import { and, asc, eq, gt, inArray, isNull, ne, or } from 'drizzle-orm'
|
||||||
|
import { alias } from 'drizzle-orm/mysql-core'
|
||||||
|
import {
|
||||||
|
AccountBalance,
|
||||||
|
AccountBalances,
|
||||||
|
AuthenticatedEncryption,
|
||||||
|
EncryptedMemo,
|
||||||
|
GradidoTransactionBuilder,
|
||||||
|
GradidoUnit,
|
||||||
|
KeyPairEd25519,
|
||||||
|
LedgerAnchor,
|
||||||
|
MemoryBlockPtr,
|
||||||
|
TransferAmount,
|
||||||
|
} from 'gradido-blockchain-js'
|
||||||
|
import * as v from 'valibot'
|
||||||
|
import { Uuidv4 } from '../../../../schemas/typeGuard.schema'
|
||||||
|
import { addToBlockchain } from '../../blockchain'
|
||||||
|
import { Context } from '../../Context'
|
||||||
|
import { TransactionTypeId } from '../../data/TransactionTypeId'
|
||||||
|
import { transactionsTable, usersTable } from '../../drizzle.schema'
|
||||||
|
import {
|
||||||
|
BlockchainError,
|
||||||
|
DatabaseError,
|
||||||
|
NegativeBalanceError,
|
||||||
|
NotEnoughGradidoBalanceError,
|
||||||
|
} from '../../errors'
|
||||||
|
import { toMysqlDateTime } from '../../utils'
|
||||||
|
import { CommunityContext, TransactionDb, transactionDbSchema, UserDb } from '../../valibot.schema'
|
||||||
|
import { AbstractSyncRole, IndexType } from './AbstractSync.role'
|
||||||
|
|
||||||
|
export class RemoteTransactionsSyncRole extends AbstractSyncRole<TransactionDb> {
|
||||||
|
constructor(context: Context) {
|
||||||
|
super(context)
|
||||||
|
this.accountBalances.reserve(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
getDate(): Date {
|
||||||
|
return this.peek().balanceDate
|
||||||
|
}
|
||||||
|
|
||||||
|
getCommunityUuids(): Uuidv4[] {
|
||||||
|
const currentItem = this.peek()
|
||||||
|
return [currentItem.user.communityUuid, currentItem.linkedUser.communityUuid]
|
||||||
|
}
|
||||||
|
|
||||||
|
getLastIndex(): IndexType {
|
||||||
|
const lastItem = this.peekLast()
|
||||||
|
return { date: lastItem.balanceDate, id: lastItem.id }
|
||||||
|
}
|
||||||
|
|
||||||
|
itemTypeName(): string {
|
||||||
|
return 'remoteTransactions'
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadFromDb(lastIndex: IndexType, count: number): Promise<TransactionDb[]> {
|
||||||
|
const linkedUsers = alias(usersTable, 'linkedUser')
|
||||||
|
const result = await this.context.db
|
||||||
|
.select({
|
||||||
|
transaction: transactionsTable,
|
||||||
|
user: usersTable,
|
||||||
|
linkedUser: linkedUsers,
|
||||||
|
})
|
||||||
|
.from(transactionsTable)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
inArray(transactionsTable.typeId, [TransactionTypeId.RECEIVE, TransactionTypeId.SEND]),
|
||||||
|
isNull(transactionsTable.transactionLinkId),
|
||||||
|
ne(usersTable.communityUuid, linkedUsers.communityUuid),
|
||||||
|
or(
|
||||||
|
gt(transactionsTable.balanceDate, toMysqlDateTime(lastIndex.date)),
|
||||||
|
and(
|
||||||
|
eq(transactionsTable.balanceDate, toMysqlDateTime(lastIndex.date)),
|
||||||
|
gt(transactionsTable.id, lastIndex.id),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.innerJoin(usersTable, eq(transactionsTable.userId, usersTable.id))
|
||||||
|
.innerJoin(linkedUsers, eq(transactionsTable.linkedUserGradidoId, linkedUsers.gradidoId))
|
||||||
|
.orderBy(asc(transactionsTable.balanceDate), asc(transactionsTable.id))
|
||||||
|
.limit(count)
|
||||||
|
|
||||||
|
return result.map((row) => {
|
||||||
|
const item = {
|
||||||
|
...row.transaction,
|
||||||
|
user: row.user,
|
||||||
|
linkedUser: row.linkedUser,
|
||||||
|
}
|
||||||
|
if (item.typeId === TransactionTypeId.SEND && item.amount) {
|
||||||
|
item.amount = new Decimal(item.amount).neg().toString()
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return v.parse(transactionDbSchema, item)
|
||||||
|
} catch (e) {
|
||||||
|
throw new DatabaseError('loadRemoteTransferTransactions', item, e as Error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTransaction(
|
||||||
|
item: TransactionDb,
|
||||||
|
senderKeyPair: KeyPairEd25519,
|
||||||
|
recipientKeyPair: KeyPairEd25519,
|
||||||
|
senderCommunityId: string,
|
||||||
|
recipientCommunityId: string,
|
||||||
|
): GradidoTransactionBuilder {
|
||||||
|
return this.transactionBuilder
|
||||||
|
.setCreatedAt(item.balanceDate)
|
||||||
|
.addMemo(
|
||||||
|
new EncryptedMemo(
|
||||||
|
item.memo,
|
||||||
|
new AuthenticatedEncryption(senderKeyPair),
|
||||||
|
new AuthenticatedEncryption(recipientKeyPair),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.setSenderCommunity(senderCommunityId)
|
||||||
|
.setRecipientCommunity(recipientCommunityId)
|
||||||
|
.setTransactionTransfer(
|
||||||
|
new TransferAmount(senderKeyPair.getPublicKey(), item.amount, senderCommunityId),
|
||||||
|
recipientKeyPair.getPublicKey(),
|
||||||
|
)
|
||||||
|
.sign(senderKeyPair)
|
||||||
|
}
|
||||||
|
|
||||||
|
calculateBalances(
|
||||||
|
item: TransactionDb,
|
||||||
|
communityContext: CommunityContext,
|
||||||
|
coinCommunityId: string,
|
||||||
|
amount: GradidoUnit,
|
||||||
|
publicKey: MemoryBlockPtr,
|
||||||
|
): AccountBalances {
|
||||||
|
this.accountBalances.clear()
|
||||||
|
|
||||||
|
// try to use same coins from this community
|
||||||
|
let lastBalance = this.getLastBalanceForUser(
|
||||||
|
publicKey,
|
||||||
|
communityContext.blockchain,
|
||||||
|
coinCommunityId,
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
coinCommunityId !== communityContext.communityId &&
|
||||||
|
(lastBalance.getBalance().equal(GradidoUnit.zero()) ||
|
||||||
|
lastBalance.getBalance().calculateDecay(lastBalance.getDate(), item.balanceDate).lt(amount))
|
||||||
|
) {
|
||||||
|
// don't work, so we use or own coins
|
||||||
|
lastBalance = this.getLastBalanceForUser(
|
||||||
|
publicKey,
|
||||||
|
communityContext.blockchain,
|
||||||
|
communityContext.communityId,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
lastBalance
|
||||||
|
.getBalance()
|
||||||
|
.calculateDecay(lastBalance.getDate(), item.balanceDate)
|
||||||
|
.add(amount)
|
||||||
|
.lt(GradidoUnit.zero()) &&
|
||||||
|
communityContext.foreign
|
||||||
|
) {
|
||||||
|
this.accountBalances.add(new AccountBalance(publicKey, GradidoUnit.zero(), coinCommunityId))
|
||||||
|
return this.accountBalances
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
lastBalance.updateLegacyDecay(amount, item.balanceDate)
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof NegativeBalanceError) {
|
||||||
|
this.logLastBalanceChangingTransactions(publicKey, communityContext.blockchain, 1)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.accountBalances.add(lastBalance.getAccountBalance())
|
||||||
|
return this.accountBalances
|
||||||
|
}
|
||||||
|
|
||||||
|
getUser(item: TransactionDb): { senderUser: UserDb; recipientUser: UserDb } {
|
||||||
|
return item.typeId === TransactionTypeId.RECEIVE
|
||||||
|
? { senderUser: item.linkedUser, recipientUser: item.user }
|
||||||
|
: { senderUser: item.user, recipientUser: item.linkedUser }
|
||||||
|
}
|
||||||
|
|
||||||
|
pushToBlockchain(item: TransactionDb): void {
|
||||||
|
const { senderUser, recipientUser } = this.getUser(item)
|
||||||
|
const ledgerAnchor = new LedgerAnchor(
|
||||||
|
item.id,
|
||||||
|
LedgerAnchor.Type_LEGACY_GRADIDO_DB_TRANSACTION_ID,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (senderUser.communityUuid === recipientUser.communityUuid) {
|
||||||
|
throw new Error(
|
||||||
|
`transfer between user from same community: ${JSON.stringify(item, null, 2)}, check db query`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const senderCommunityContext = this.context.getCommunityContextByUuid(senderUser.communityUuid)
|
||||||
|
const recipientCommunityContext = this.context.getCommunityContextByUuid(
|
||||||
|
recipientUser.communityUuid,
|
||||||
|
)
|
||||||
|
const senderBlockchain = senderCommunityContext.blockchain
|
||||||
|
const recipientBlockchain = recipientCommunityContext.blockchain
|
||||||
|
|
||||||
|
// I use the received transaction so user and linked user are swapped and user is recipient and linkedUser ist sender
|
||||||
|
const senderKeyPair = this.getAccountKeyPair(senderCommunityContext, senderUser.gradidoId)
|
||||||
|
const senderPublicKey = senderKeyPair.getPublicKey()
|
||||||
|
const recipientKeyPair = this.getAccountKeyPair(
|
||||||
|
recipientCommunityContext,
|
||||||
|
recipientUser.gradidoId,
|
||||||
|
)
|
||||||
|
const recipientPublicKey = recipientKeyPair.getPublicKey()
|
||||||
|
|
||||||
|
if (!senderKeyPair || !senderPublicKey || !recipientKeyPair || !recipientPublicKey) {
|
||||||
|
throw new Error(`missing key for ${this.itemTypeName()}: ${JSON.stringify(item, null, 2)}`)
|
||||||
|
}
|
||||||
|
const transactionBuilder = this.buildTransaction(
|
||||||
|
item,
|
||||||
|
senderKeyPair,
|
||||||
|
recipientKeyPair,
|
||||||
|
senderCommunityContext.communityId,
|
||||||
|
recipientCommunityContext.communityId,
|
||||||
|
)
|
||||||
|
const outboundTransaction = transactionBuilder.buildOutbound()
|
||||||
|
|
||||||
|
try {
|
||||||
|
addToBlockchain(
|
||||||
|
outboundTransaction,
|
||||||
|
senderBlockchain,
|
||||||
|
ledgerAnchor,
|
||||||
|
this.calculateBalances(
|
||||||
|
item,
|
||||||
|
senderCommunityContext,
|
||||||
|
senderCommunityContext.communityId,
|
||||||
|
item.amount.negated(),
|
||||||
|
senderPublicKey,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof NotEnoughGradidoBalanceError) {
|
||||||
|
this.logLastBalanceChangingTransactions(senderPublicKey, senderBlockchain)
|
||||||
|
}
|
||||||
|
throw new BlockchainError(`Error adding ${this.itemTypeName()}`, item, e as Error)
|
||||||
|
}
|
||||||
|
transactionBuilder.setParentLedgerAnchor(ledgerAnchor)
|
||||||
|
const inboundTransaction = transactionBuilder.buildInbound()
|
||||||
|
try {
|
||||||
|
addToBlockchain(
|
||||||
|
inboundTransaction,
|
||||||
|
recipientBlockchain,
|
||||||
|
ledgerAnchor,
|
||||||
|
this.calculateBalances(
|
||||||
|
item,
|
||||||
|
recipientCommunityContext,
|
||||||
|
senderCommunityContext.communityId,
|
||||||
|
item.amount,
|
||||||
|
recipientPublicKey,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof NotEnoughGradidoBalanceError) {
|
||||||
|
this.logLastBalanceChangingTransactions(recipientPublicKey, recipientBlockchain)
|
||||||
|
}
|
||||||
|
throw new BlockchainError(`Error adding ${this.itemTypeName()}`, item, e as Error)
|
||||||
|
}
|
||||||
|
// this.logLastBalanceChangingTransactions(senderPublicKey, senderCommunityContext.blockchain, 1)
|
||||||
|
// this.logLastBalanceChangingTransactions(recipientPublicKey, recipientCommunityContext.blockchain, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,229 @@
|
|||||||
|
import Decimal from 'decimal.js-light'
|
||||||
|
import { and, asc, eq, gt, or } from 'drizzle-orm'
|
||||||
|
import {
|
||||||
|
AccountBalance,
|
||||||
|
AccountBalances,
|
||||||
|
AuthenticatedEncryption,
|
||||||
|
DurationSeconds,
|
||||||
|
EncryptedMemo,
|
||||||
|
Filter,
|
||||||
|
GradidoTransactionBuilder,
|
||||||
|
GradidoTransfer,
|
||||||
|
GradidoUnit,
|
||||||
|
KeyPairEd25519,
|
||||||
|
LedgerAnchor,
|
||||||
|
MemoryBlockPtr,
|
||||||
|
SearchDirection_DESC,
|
||||||
|
TransferAmount,
|
||||||
|
transactionTypeToString,
|
||||||
|
} from 'gradido-blockchain-js'
|
||||||
|
import * as v from 'valibot'
|
||||||
|
import { deriveFromCode } from '../../../../data/deriveKeyPair'
|
||||||
|
import { Uuidv4 } from '../../../../schemas/typeGuard.schema'
|
||||||
|
import { addToBlockchain } from '../../blockchain'
|
||||||
|
import { Context } from '../../Context'
|
||||||
|
import { transactionLinksTable, usersTable } from '../../drizzle.schema'
|
||||||
|
import { BlockchainError, DatabaseError, NegativeBalanceError } from '../../errors'
|
||||||
|
import { reverseLegacyDecay, toMysqlDateTime } from '../../utils'
|
||||||
|
import { CommunityContext, TransactionLinkDb, transactionLinkDbSchema } from '../../valibot.schema'
|
||||||
|
import { AbstractSyncRole, IndexType } from './AbstractSync.role'
|
||||||
|
|
||||||
|
export class TransactionLinkFundingsSyncRole extends AbstractSyncRole<TransactionLinkDb> {
|
||||||
|
constructor(context: Context) {
|
||||||
|
super(context)
|
||||||
|
this.accountBalances.reserve(2)
|
||||||
|
}
|
||||||
|
getDate(): Date {
|
||||||
|
return this.peek().createdAt
|
||||||
|
}
|
||||||
|
|
||||||
|
getCommunityUuids(): Uuidv4[] {
|
||||||
|
return [this.peek().user.communityUuid]
|
||||||
|
}
|
||||||
|
|
||||||
|
getLastIndex(): IndexType {
|
||||||
|
const lastItem = this.peekLast()
|
||||||
|
return { date: lastItem.createdAt, id: lastItem.id }
|
||||||
|
}
|
||||||
|
|
||||||
|
itemTypeName(): string {
|
||||||
|
return 'transactionLinkFundings'
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadFromDb(lastIndex: IndexType, count: number): Promise<TransactionLinkDb[]> {
|
||||||
|
const result = await this.context.db
|
||||||
|
.select()
|
||||||
|
.from(transactionLinksTable)
|
||||||
|
.innerJoin(usersTable, eq(transactionLinksTable.userId, usersTable.id))
|
||||||
|
.where(
|
||||||
|
or(
|
||||||
|
gt(transactionLinksTable.createdAt, toMysqlDateTime(lastIndex.date)),
|
||||||
|
and(
|
||||||
|
eq(transactionLinksTable.createdAt, toMysqlDateTime(lastIndex.date)),
|
||||||
|
gt(transactionLinksTable.id, lastIndex.id),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.orderBy(asc(transactionLinksTable.createdAt), asc(transactionLinksTable.id))
|
||||||
|
.limit(count)
|
||||||
|
|
||||||
|
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(
|
||||||
|
communityContext: CommunityContext,
|
||||||
|
item: TransactionLinkDb,
|
||||||
|
blockedAmount: GradidoUnit,
|
||||||
|
duration: DurationSeconds,
|
||||||
|
senderKeyPair: KeyPairEd25519,
|
||||||
|
recipientKeyPair: KeyPairEd25519,
|
||||||
|
): GradidoTransactionBuilder {
|
||||||
|
return this.transactionBuilder
|
||||||
|
.setCreatedAt(item.createdAt)
|
||||||
|
.addMemo(
|
||||||
|
new EncryptedMemo(
|
||||||
|
item.memo,
|
||||||
|
new AuthenticatedEncryption(senderKeyPair),
|
||||||
|
new AuthenticatedEncryption(recipientKeyPair),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.setSenderCommunity(communityContext.communityId)
|
||||||
|
.setDeferredTransfer(
|
||||||
|
new GradidoTransfer(
|
||||||
|
new TransferAmount(
|
||||||
|
senderKeyPair.getPublicKey(),
|
||||||
|
blockedAmount,
|
||||||
|
communityContext.communityId,
|
||||||
|
),
|
||||||
|
recipientKeyPair.getPublicKey(),
|
||||||
|
),
|
||||||
|
duration,
|
||||||
|
)
|
||||||
|
.sign(senderKeyPair)
|
||||||
|
}
|
||||||
|
|
||||||
|
calculateBalances(
|
||||||
|
item: TransactionLinkDb,
|
||||||
|
blockedAmount: GradidoUnit,
|
||||||
|
communityContext: CommunityContext,
|
||||||
|
senderPublicKey: MemoryBlockPtr,
|
||||||
|
recipientPublicKey: MemoryBlockPtr,
|
||||||
|
): AccountBalances {
|
||||||
|
this.accountBalances.clear()
|
||||||
|
const senderLastBalance = this.getLastBalanceForUser(
|
||||||
|
senderPublicKey,
|
||||||
|
communityContext.blockchain,
|
||||||
|
communityContext.communityId,
|
||||||
|
)
|
||||||
|
try {
|
||||||
|
senderLastBalance.updateLegacyDecay(blockedAmount.negated(), item.createdAt)
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof NegativeBalanceError) {
|
||||||
|
this.logLastBalanceChangingTransactions(senderPublicKey, communityContext.blockchain)
|
||||||
|
this.context.logger.debug(`sender public key: ${senderPublicKey.convertToHex()}`)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.accountBalances.add(senderLastBalance.getAccountBalance())
|
||||||
|
this.accountBalances.add(
|
||||||
|
new AccountBalance(recipientPublicKey, blockedAmount, communityContext.communityId),
|
||||||
|
)
|
||||||
|
return this.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,
|
||||||
|
)
|
||||||
|
let blockedAmount = GradidoUnit.fromString(
|
||||||
|
reverseLegacyDecay(new Decimal(item.amount.toString()), duration.getSeconds()).toString(),
|
||||||
|
)
|
||||||
|
let accountBalances: AccountBalances
|
||||||
|
try {
|
||||||
|
accountBalances = this.calculateBalances(
|
||||||
|
item,
|
||||||
|
blockedAmount,
|
||||||
|
communityContext,
|
||||||
|
senderPublicKey,
|
||||||
|
recipientPublicKey,
|
||||||
|
)
|
||||||
|
} catch (e) {
|
||||||
|
if (item.deletedAt && e instanceof NegativeBalanceError) {
|
||||||
|
const senderLastBalance = this.getLastBalanceForUser(
|
||||||
|
senderPublicKey,
|
||||||
|
communityContext.blockchain,
|
||||||
|
communityContext.communityId,
|
||||||
|
)
|
||||||
|
senderLastBalance.updateLegacyDecay(GradidoUnit.zero(), item.createdAt)
|
||||||
|
const oldBlockedAmountString = blockedAmount.toString()
|
||||||
|
blockedAmount = senderLastBalance.getBalance()
|
||||||
|
accountBalances = this.calculateBalances(
|
||||||
|
item,
|
||||||
|
blockedAmount,
|
||||||
|
communityContext,
|
||||||
|
senderPublicKey,
|
||||||
|
recipientPublicKey,
|
||||||
|
)
|
||||||
|
this.context.logger.warn(
|
||||||
|
`workaround: fix founding for deleted link, reduce funding to actual sender balance: ${senderPublicKey.convertToHex()}: from ${oldBlockedAmountString} GDD to ${blockedAmount.toString()} GDD`,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
this.context.logger.error(
|
||||||
|
`error calculate account balances for ${this.itemTypeName()}: ${JSON.stringify(item, null, 2)}`,
|
||||||
|
)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
addToBlockchain(
|
||||||
|
this.buildTransaction(
|
||||||
|
communityContext,
|
||||||
|
item,
|
||||||
|
blockedAmount,
|
||||||
|
duration,
|
||||||
|
senderKeyPair,
|
||||||
|
recipientKeyPair,
|
||||||
|
).build(),
|
||||||
|
blockchain,
|
||||||
|
new LedgerAnchor(item.id, LedgerAnchor.Type_LEGACY_GRADIDO_DB_TRANSACTION_LINK_ID),
|
||||||
|
accountBalances,
|
||||||
|
)
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof NegativeBalanceError) {
|
||||||
|
if (
|
||||||
|
!item.deletedAt &&
|
||||||
|
!item.redeemedAt &&
|
||||||
|
item.validUntil.getTime() < new Date().getTime()
|
||||||
|
) {
|
||||||
|
this.context.logger.warn(
|
||||||
|
`TransactionLinks: ${item.id} skipped, because else it lead to negative balance error, but it wasn't used.`,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new BlockchainError(`Error adding ${this.itemTypeName()}`, item, e as Error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,132 @@
|
|||||||
|
import { and, asc, eq, gt, isNotNull, or } from 'drizzle-orm'
|
||||||
|
import {
|
||||||
|
AccountBalance,
|
||||||
|
AccountBalances,
|
||||||
|
AddressType_COMMUNITY_HUMAN,
|
||||||
|
GradidoTransactionBuilder,
|
||||||
|
GradidoUnit,
|
||||||
|
KeyPairEd25519,
|
||||||
|
LedgerAnchor,
|
||||||
|
MemoryBlockPtr,
|
||||||
|
} from 'gradido-blockchain-js'
|
||||||
|
import * as v from 'valibot'
|
||||||
|
import { deriveFromKeyPairAndUuid } from '../../../../data/deriveKeyPair'
|
||||||
|
import { Uuidv4Hash } from '../../../../data/Uuidv4Hash'
|
||||||
|
import { Uuidv4 } from '../../../../schemas/typeGuard.schema'
|
||||||
|
import { addToBlockchain } from '../../blockchain'
|
||||||
|
import { Context } from '../../Context'
|
||||||
|
import { usersTable } from '../../drizzle.schema'
|
||||||
|
import { BlockchainError, DatabaseError } from '../../errors'
|
||||||
|
import { toMysqlDateTime } from '../../utils'
|
||||||
|
import { CommunityContext, UserDb, userDbSchema } from '../../valibot.schema'
|
||||||
|
import { AbstractSyncRole, IndexType } from './AbstractSync.role'
|
||||||
|
|
||||||
|
export class UsersSyncRole extends AbstractSyncRole<UserDb> {
|
||||||
|
constructor(context: Context) {
|
||||||
|
super(context)
|
||||||
|
this.accountBalances.reserve(1)
|
||||||
|
}
|
||||||
|
getDate(): Date {
|
||||||
|
return this.peek().createdAt
|
||||||
|
}
|
||||||
|
|
||||||
|
getCommunityUuids(): Uuidv4[] {
|
||||||
|
return [this.peek().communityUuid]
|
||||||
|
}
|
||||||
|
|
||||||
|
getLastIndex(): IndexType {
|
||||||
|
const lastItem = this.peekLast()
|
||||||
|
return { date: lastItem.createdAt, id: lastItem.id }
|
||||||
|
}
|
||||||
|
|
||||||
|
itemTypeName(): string {
|
||||||
|
return 'users'
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadFromDb(lastIndex: IndexType, count: number): Promise<UserDb[]> {
|
||||||
|
const result = await this.context.db
|
||||||
|
.select()
|
||||||
|
.from(usersTable)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
or(
|
||||||
|
gt(usersTable.createdAt, toMysqlDateTime(lastIndex.date)),
|
||||||
|
and(
|
||||||
|
eq(usersTable.createdAt, toMysqlDateTime(lastIndex.date)),
|
||||||
|
gt(usersTable.id, lastIndex.id),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
isNotNull(usersTable.communityUuid),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.orderBy(asc(usersTable.createdAt), asc(usersTable.id))
|
||||||
|
.limit(count)
|
||||||
|
|
||||||
|
return result.map((row) => {
|
||||||
|
try {
|
||||||
|
return v.parse(userDbSchema, row)
|
||||||
|
} catch (e) {
|
||||||
|
throw new DatabaseError('loadUsers', row, e as Error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTransaction(
|
||||||
|
communityContext: CommunityContext,
|
||||||
|
item: UserDb,
|
||||||
|
communityKeyPair: KeyPairEd25519,
|
||||||
|
accountKeyPair: KeyPairEd25519,
|
||||||
|
userKeyPair: KeyPairEd25519,
|
||||||
|
): GradidoTransactionBuilder {
|
||||||
|
return this.transactionBuilder
|
||||||
|
.setCreatedAt(item.createdAt)
|
||||||
|
.setSenderCommunity(communityContext.communityId)
|
||||||
|
.setRegisterAddress(
|
||||||
|
userKeyPair.getPublicKey(),
|
||||||
|
AddressType_COMMUNITY_HUMAN,
|
||||||
|
new Uuidv4Hash(item.gradidoId).getAsMemoryBlock(),
|
||||||
|
accountKeyPair.getPublicKey(),
|
||||||
|
)
|
||||||
|
.sign(communityKeyPair)
|
||||||
|
.sign(accountKeyPair)
|
||||||
|
.sign(userKeyPair)
|
||||||
|
}
|
||||||
|
|
||||||
|
calculateAccountBalances(
|
||||||
|
accountPublicKey: MemoryBlockPtr,
|
||||||
|
communityContext: CommunityContext,
|
||||||
|
): AccountBalances {
|
||||||
|
this.accountBalances.clear()
|
||||||
|
this.accountBalances.add(
|
||||||
|
new AccountBalance(accountPublicKey, GradidoUnit.zero(), communityContext.communityId),
|
||||||
|
)
|
||||||
|
return this.accountBalances
|
||||||
|
}
|
||||||
|
|
||||||
|
pushToBlockchain(item: UserDb): void {
|
||||||
|
const communityContext = this.context.getCommunityContextByUuid(item.communityUuid)
|
||||||
|
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(
|
||||||
|
communityContext,
|
||||||
|
item,
|
||||||
|
communityContext.keyPair,
|
||||||
|
accountKeyPair,
|
||||||
|
userKeyPair,
|
||||||
|
).build(),
|
||||||
|
communityContext.blockchain,
|
||||||
|
new LedgerAnchor(item.id, LedgerAnchor.Type_LEGACY_GRADIDO_DB_USER_ID),
|
||||||
|
this.calculateAccountBalances(accountPublicKey, communityContext),
|
||||||
|
)
|
||||||
|
} catch (e) {
|
||||||
|
throw new BlockchainError(`Error adding ${this.itemTypeName()}`, item, e as Error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,161 @@
|
|||||||
|
import {
|
||||||
|
Abstract,
|
||||||
|
Filter,
|
||||||
|
InteractionCreateTransactionByEvent,
|
||||||
|
LedgerAnchor,
|
||||||
|
Profiler,
|
||||||
|
Timestamp,
|
||||||
|
} from 'gradido-blockchain-js'
|
||||||
|
import { Logger } from 'log4js'
|
||||||
|
import { callTime } from '../../blockchain'
|
||||||
|
import { Context } from '../../Context'
|
||||||
|
import { CommunityContext } from '../../valibot.schema'
|
||||||
|
import { nanosBalanceForUser } from './AbstractSync.role'
|
||||||
|
import { ContributionLinkTransactionSyncRole } from './ContributionLinkTransactionSync.role'
|
||||||
|
import { CreationsSyncRole } from './CreationsSync.role'
|
||||||
|
import { DeletedTransactionLinksSyncRole } from './DeletedTransactionLinksSync.role'
|
||||||
|
import { LocalTransactionsSyncRole } from './LocalTransactionsSync.role'
|
||||||
|
import { RedeemTransactionLinksSyncRole } from './RedeemTransactionLinksSync.role'
|
||||||
|
import { RemoteTransactionsSyncRole } from './RemoteTransactionsSync.role'
|
||||||
|
import { TransactionLinkFundingsSyncRole } from './TransactionLinkFundingsSync.role'
|
||||||
|
import { UsersSyncRole } from './UsersSync.role'
|
||||||
|
|
||||||
|
function processTransactionTrigger(context: CommunityContext, endDate: Date, logger: Logger) {
|
||||||
|
while (true) {
|
||||||
|
const lastTx = context.blockchain.findOne(Filter.LAST_TRANSACTION)
|
||||||
|
let confirmedAt: Timestamp | undefined
|
||||||
|
if (!lastTx) {
|
||||||
|
// no transaction, no triggers
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
const confirmedTx = lastTx.getConfirmedTransaction()
|
||||||
|
if (!confirmedTx) {
|
||||||
|
throw new Error('missing confirmed tx in transaction entry')
|
||||||
|
}
|
||||||
|
confirmedAt = confirmedTx.getConfirmedAt()
|
||||||
|
}
|
||||||
|
const triggerEvent = context.blockchain.findNextTransactionTriggerEventInRange(
|
||||||
|
confirmedAt,
|
||||||
|
new Timestamp(endDate),
|
||||||
|
)
|
||||||
|
if (!triggerEvent) {
|
||||||
|
// no trigger, we can exit here
|
||||||
|
return
|
||||||
|
}
|
||||||
|
context.blockchain.removeTransactionTriggerEvent(triggerEvent)
|
||||||
|
try {
|
||||||
|
// InMemoryBlockchain extend Abstract, but between C++ -> Swig -> TypeScript it seems the info is gone, so I need to cheat a bit here
|
||||||
|
const createTransactionByEvent = new InteractionCreateTransactionByEvent(
|
||||||
|
context.blockchain as unknown as Abstract,
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
!context.blockchain.createAndAddConfirmedTransaction(
|
||||||
|
createTransactionByEvent.run(triggerEvent),
|
||||||
|
new LedgerAnchor(
|
||||||
|
triggerEvent.getLinkedTransactionId(),
|
||||||
|
LedgerAnchor.Type_NODE_TRIGGER_TRANSACTION_ID,
|
||||||
|
),
|
||||||
|
triggerEvent.getTargetDate(),
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw new Error('Adding trigger created Transaction Failed')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
context.blockchain.addTransactionTriggerEvent(triggerEvent)
|
||||||
|
logger.error(
|
||||||
|
`Error processing transaction trigger event for transaction: ${triggerEvent.getLinkedTransactionId()}`,
|
||||||
|
)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function syncDbWithBlockchainContext(context: Context, batchSize: number) {
|
||||||
|
const timeUsedDB = new Profiler()
|
||||||
|
const timeUsedBlockchain = new Profiler()
|
||||||
|
const timeUsedAll = new Profiler()
|
||||||
|
const timeBetweenPrints = new Profiler()
|
||||||
|
const containers = [
|
||||||
|
new UsersSyncRole(context),
|
||||||
|
new CreationsSyncRole(context),
|
||||||
|
new LocalTransactionsSyncRole(context),
|
||||||
|
new TransactionLinkFundingsSyncRole(context),
|
||||||
|
new RedeemTransactionLinksSyncRole(context),
|
||||||
|
new ContributionLinkTransactionSyncRole(context),
|
||||||
|
new DeletedTransactionLinksSyncRole(context),
|
||||||
|
new RemoteTransactionsSyncRole(context),
|
||||||
|
]
|
||||||
|
let transactionsCount = 0
|
||||||
|
let transactionsCountSinceLastLog = 0
|
||||||
|
let transactionsCountSinceLastPrint = 0
|
||||||
|
let available = containers
|
||||||
|
const isDebug = context.logger.isDebugEnabled()
|
||||||
|
let lastPrintedCallTime = 0
|
||||||
|
while (true) {
|
||||||
|
timeUsedDB.reset()
|
||||||
|
const results = await Promise.all(available.map((c) => c.ensureFilled(batchSize)))
|
||||||
|
const loadedItemsCount = results.reduce((acc, c) => acc + c, 0)
|
||||||
|
// log only, if at least one new item was loaded
|
||||||
|
if (loadedItemsCount && isDebug) {
|
||||||
|
context.logger.debug(`${loadedItemsCount} new items loaded from db in ${timeUsedDB.string()}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove empty containers
|
||||||
|
available = available.filter((c) => !c.isEmpty())
|
||||||
|
if (available.length === 0) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// sort by date, to ensure container on index 0 is the one with the smallest date
|
||||||
|
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()}`)
|
||||||
|
}
|
||||||
|
const communityUuids = available[0].getCommunityUuids()
|
||||||
|
for (let i = 0; i < communityUuids.length; ++i) {
|
||||||
|
processTransactionTrigger(
|
||||||
|
context.getCommunityContextByUuid(communityUuids[i]),
|
||||||
|
available[0].getDate(),
|
||||||
|
context.logger,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
available[0].toBlockchain()
|
||||||
|
transactionsCount++
|
||||||
|
if (isDebug) {
|
||||||
|
if (timeBetweenPrints.millis() > 100) {
|
||||||
|
process.stdout.write(`successfully added to blockchain: ${transactionsCount}\r`)
|
||||||
|
timeBetweenPrints.reset()
|
||||||
|
}
|
||||||
|
transactionsCountSinceLastLog++
|
||||||
|
if (transactionsCountSinceLastLog >= batchSize) {
|
||||||
|
context.logger.debug(
|
||||||
|
`${transactionsCountSinceLastLog} transactions added to blockchain in ${timeUsedBlockchain.string()}`,
|
||||||
|
)
|
||||||
|
context.logger.info(
|
||||||
|
`Time for createAndConfirm: ${((callTime - lastPrintedCallTime) / 1000 / 1000).toFixed(2)} milliseconds`,
|
||||||
|
)
|
||||||
|
lastPrintedCallTime = callTime
|
||||||
|
timeUsedBlockchain.reset()
|
||||||
|
transactionsCountSinceLastLog = 0
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
transactionsCountSinceLastPrint++
|
||||||
|
if (transactionsCountSinceLastPrint >= 100) {
|
||||||
|
process.stdout.write(`successfully added to blockchain: ${transactionsCount}\r`)
|
||||||
|
transactionsCountSinceLastPrint = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
process.stdout.write(`successfully added to blockchain: ${transactionsCount}\n`)
|
||||||
|
context.logger.info(
|
||||||
|
`Synced ${transactionsCount} transactions to blockchain in ${timeUsedAll.string()}`,
|
||||||
|
)
|
||||||
|
context.logger.info(
|
||||||
|
`Time for createAndConfirm: ${(callTime / 1000 / 1000 / 1000).toFixed(2)} seconds`,
|
||||||
|
)
|
||||||
|
context.logger.info(
|
||||||
|
`Time for call lastBalance of user: ${(nanosBalanceForUser / 1000 / 1000 / 1000).toFixed(2)} seconds`,
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -0,0 +1,70 @@
|
|||||||
|
import Decimal from 'decimal.js-light'
|
||||||
|
import { crypto_generichash_batch, crypto_generichash_KEYBYTES } from 'sodium-native'
|
||||||
|
|
||||||
|
export function bytesToMbyte(bytes: number): string {
|
||||||
|
return (bytes / 1024 / 1024).toFixed(4)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function bytesToKbyte(bytes: number): string {
|
||||||
|
return (bytes / 1024).toFixed(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function bytesString(bytes: number): string {
|
||||||
|
if (bytes > 1024 * 1024) {
|
||||||
|
return `${bytesToMbyte(bytes)} MB`
|
||||||
|
} else if (bytes > 1024) {
|
||||||
|
return `${bytesToKbyte(bytes)} KB`
|
||||||
|
}
|
||||||
|
return `${bytes.toFixed(0)} Bytes`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toMysqlDateTime(date: Date): string {
|
||||||
|
return date.toISOString().slice(0, 23).replace('T', ' ')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function calculateOneHashStep(hash: Buffer, data: Buffer): Buffer<ArrayBuffer> {
|
||||||
|
const outputHash = Buffer.alloc(crypto_generichash_KEYBYTES, 0)
|
||||||
|
crypto_generichash_batch(outputHash, [hash, data])
|
||||||
|
return outputHash
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DECAY_START_TIME = new Date('2021-05-13T17:46:31Z')
|
||||||
|
export const SECONDS_PER_YEAR_GREGORIAN_CALENDER = 31556952.0
|
||||||
|
const FACTOR = new Decimal('0.99999997803504048973201202316767079413460520837376')
|
||||||
|
|
||||||
|
export function legacyDecayFormula(value: Decimal, seconds: number): Decimal {
|
||||||
|
// TODO why do we need to convert this here to a string to work properly?
|
||||||
|
// chatgpt: We convert to string here to avoid precision loss:
|
||||||
|
// .pow(seconds) can internally round the result, especially for large values of `seconds`.
|
||||||
|
// Using .toString() ensures full precision is preserved in the multiplication.
|
||||||
|
return value.mul(FACTOR.pow(seconds).toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
export function reverseLegacyDecay(result: Decimal, seconds: number): Decimal {
|
||||||
|
return result.div(FACTOR.pow(seconds).toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
export function legacyCalculateDecay(amount: Decimal, from: Date, to: Date): Decimal {
|
||||||
|
const fromMs = from.getTime()
|
||||||
|
const toMs = to.getTime()
|
||||||
|
const startBlockMs = DECAY_START_TIME.getTime()
|
||||||
|
|
||||||
|
if (toMs < fromMs) {
|
||||||
|
throw new Error(
|
||||||
|
`calculateDecay: to (${to.toISOString()}) < from (${from.toISOString()}), reverse decay calculation is invalid`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// decay started after end date; no decay
|
||||||
|
if (startBlockMs > toMs) {
|
||||||
|
return amount
|
||||||
|
}
|
||||||
|
// decay started before start date; decay for full duration
|
||||||
|
let duration = (toMs - fromMs) / 1000
|
||||||
|
|
||||||
|
// decay started between start and end date; decay from decay start till end date
|
||||||
|
if (startBlockMs >= fromMs) {
|
||||||
|
duration = (toMs - startBlockMs) / 1000
|
||||||
|
}
|
||||||
|
return legacyDecayFormula(amount, duration)
|
||||||
|
}
|
||||||
@ -0,0 +1,174 @@
|
|||||||
|
import Decimal from 'decimal.js-light'
|
||||||
|
import { GradidoUnit, InMemoryBlockchain, KeyPairEd25519 } from 'gradido-blockchain-js'
|
||||||
|
import * as v from 'valibot'
|
||||||
|
import { booleanSchema, dateSchema } from '../../schemas/typeConverter.schema'
|
||||||
|
import {
|
||||||
|
gradidoAmountSchema,
|
||||||
|
identifierSeedSchema,
|
||||||
|
memoSchema,
|
||||||
|
uuidv4Schema,
|
||||||
|
} from '../../schemas/typeGuard.schema'
|
||||||
|
import { Balance } from './data/Balance'
|
||||||
|
import { TransactionTypeId } from './data/TransactionTypeId'
|
||||||
|
|
||||||
|
const positiveNumberSchema = v.pipe(v.number(), v.minValue(1))
|
||||||
|
|
||||||
|
export const userDbSchema = v.object({
|
||||||
|
id: positiveNumberSchema,
|
||||||
|
gradidoId: uuidv4Schema,
|
||||||
|
communityUuid: uuidv4Schema,
|
||||||
|
createdAt: dateSchema,
|
||||||
|
})
|
||||||
|
/*
|
||||||
|
declare const validLegacyAmount: unique symbol
|
||||||
|
export type LegacyAmount = string & { [validLegacyAmount]: true }
|
||||||
|
|
||||||
|
export const legacyAmountSchema = v.pipe(
|
||||||
|
v.string(),
|
||||||
|
v.regex(/^-?[0-9]+(\.[0-9]+)?$/),
|
||||||
|
v.transform<string, LegacyAmount>((input: string) => input as LegacyAmount),
|
||||||
|
)
|
||||||
|
|
||||||
|
declare const validGradidoAmount: unique symbol
|
||||||
|
export type GradidoAmount = GradidoUnit & { [validGradidoAmount]: true }
|
||||||
|
|
||||||
|
export const gradidoAmountSchema = v.pipe(
|
||||||
|
v.union([legacyAmountSchema, v.instance(GradidoUnit, 'expect GradidoUnit type')]),
|
||||||
|
v.transform<LegacyAmount | GradidoUnit, GradidoAmount>((input: LegacyAmount | GradidoUnit) => {
|
||||||
|
if (input instanceof GradidoUnit) {
|
||||||
|
return input as GradidoAmount
|
||||||
|
}
|
||||||
|
// round floor with decimal js beforehand
|
||||||
|
const rounded = new Decimal(input).toDecimalPlaces(4, Decimal.ROUND_FLOOR).toString()
|
||||||
|
return GradidoUnit.fromString(rounded) as GradidoAmount
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
*/
|
||||||
|
export const transactionBaseSchema = v.object({
|
||||||
|
id: positiveNumberSchema,
|
||||||
|
amount: gradidoAmountSchema,
|
||||||
|
memo: memoSchema,
|
||||||
|
user: userDbSchema,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const transactionDbSchema = v.pipe(
|
||||||
|
v.object({
|
||||||
|
...transactionBaseSchema.entries,
|
||||||
|
typeId: v.enum(TransactionTypeId),
|
||||||
|
balanceDate: dateSchema,
|
||||||
|
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)}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// check that user and linked user exist before transaction balance date
|
||||||
|
const balanceDate = new Date(value.balanceDate)
|
||||||
|
if (
|
||||||
|
value.user.createdAt.getTime() >= balanceDate.getTime() ||
|
||||||
|
value.linkedUser?.createdAt.getTime() >= balanceDate.getTime()
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
`at least one user was created after transaction balance date, logic error! ${JSON.stringify(value, null, 2)}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return value
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
export const creationTransactionDbSchema = v.pipe(
|
||||||
|
v.object({
|
||||||
|
...transactionBaseSchema.entries,
|
||||||
|
contributionDate: dateSchema,
|
||||||
|
confirmedAt: dateSchema,
|
||||||
|
confirmedByUser: userDbSchema,
|
||||||
|
transactionId: positiveNumberSchema,
|
||||||
|
}),
|
||||||
|
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({
|
||||||
|
...transactionBaseSchema.entries,
|
||||||
|
code: identifierSeedSchema,
|
||||||
|
createdAt: dateSchema,
|
||||||
|
validUntil: dateSchema,
|
||||||
|
holdAvailableAmount: gradidoAmountSchema,
|
||||||
|
redeemedAt: v.nullish(dateSchema),
|
||||||
|
deletedAt: v.nullish(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),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const communityContextSchema = v.object({
|
||||||
|
communityId: v.string(),
|
||||||
|
foreign: booleanSchema,
|
||||||
|
blockchain: v.instance(InMemoryBlockchain, 'expect InMemoryBlockchain type'),
|
||||||
|
keyPair: v.instance(KeyPairEd25519),
|
||||||
|
folder: v.pipe(
|
||||||
|
v.string(),
|
||||||
|
v.minLength(1, 'expect string length >= 1'),
|
||||||
|
v.maxLength(512, 'expect string length <= 512'),
|
||||||
|
v.regex(/^[a-zA-Z0-9-_]+$/, 'expect string to be a valid (alphanumeric, _, -) folder name'),
|
||||||
|
),
|
||||||
|
gmwBalance: v.instance(Balance),
|
||||||
|
aufBalance: v.instance(Balance),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type TransactionDb = v.InferOutput<typeof transactionDbSchema>
|
||||||
|
export type CreationTransactionDb = v.InferOutput<typeof creationTransactionDbSchema>
|
||||||
|
export type UserDb = v.InferOutput<typeof userDbSchema>
|
||||||
|
export type TransactionLinkDb = v.InferOutput<typeof transactionLinkDbSchema>
|
||||||
|
export type RedeemedTransactionLinkDb = v.InferOutput<typeof redeemedTransactionLinkDbSchema>
|
||||||
|
export type DeletedTransactionLinkDb = v.InferOutput<typeof deletedTransactionLinKDbSchema>
|
||||||
|
export type CommunityDb = v.InferOutput<typeof communityDbSchema>
|
||||||
|
export type CommunityContext = v.InferOutput<typeof communityContextSchema>
|
||||||
@ -11,6 +11,7 @@ export type IdentifierCommunityAccount = v.InferOutput<typeof identifierCommunit
|
|||||||
|
|
||||||
export const identifierKeyPairSchema = v.object({
|
export const identifierKeyPairSchema = v.object({
|
||||||
communityTopicId: hieroIdSchema,
|
communityTopicId: hieroIdSchema,
|
||||||
|
communityId: uuidv4Schema,
|
||||||
account: v.optional(identifierCommunityAccountSchema),
|
account: v.optional(identifierCommunityAccountSchema),
|
||||||
seed: v.optional(identifierSeedSchema),
|
seed: v.optional(identifierSeedSchema),
|
||||||
})
|
})
|
||||||
|
|||||||
@ -34,8 +34,12 @@ const transactionLinkCode = (date: Date): string => {
|
|||||||
}
|
}
|
||||||
let topic: HieroId
|
let topic: HieroId
|
||||||
const topicString = '0.0.261'
|
const topicString = '0.0.261'
|
||||||
|
let communityUuid: Uuidv4
|
||||||
|
const communityUuidString = 'fcd48487-6d31-4f4c-be9b-b3c8ca853912'
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
topic = v.parse(hieroIdSchema, topicString)
|
topic = v.parse(hieroIdSchema, topicString)
|
||||||
|
communityUuid = v.parse(uuidv4Schema, communityUuidString)
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('transaction schemas', () => {
|
describe('transaction schemas', () => {
|
||||||
@ -55,6 +59,7 @@ describe('transaction schemas', () => {
|
|||||||
registerAddress = {
|
registerAddress = {
|
||||||
user: {
|
user: {
|
||||||
communityTopicId: topicString,
|
communityTopicId: topicString,
|
||||||
|
communityId: communityUuidString,
|
||||||
account: { userUuid: userUuidString },
|
account: { userUuid: userUuidString },
|
||||||
},
|
},
|
||||||
type: InputTransactionType.REGISTER_ADDRESS,
|
type: InputTransactionType.REGISTER_ADDRESS,
|
||||||
@ -66,6 +71,7 @@ describe('transaction schemas', () => {
|
|||||||
expect(v.parse(transactionSchema, registerAddress)).toEqual({
|
expect(v.parse(transactionSchema, registerAddress)).toEqual({
|
||||||
user: {
|
user: {
|
||||||
communityTopicId: topic,
|
communityTopicId: topic,
|
||||||
|
communityId: communityUuid,
|
||||||
account: {
|
account: {
|
||||||
userUuid,
|
userUuid,
|
||||||
accountNr: 0,
|
accountNr: 0,
|
||||||
@ -80,6 +86,7 @@ describe('transaction schemas', () => {
|
|||||||
expect(v.parse(registerAddressTransactionSchema, registerAddress)).toEqual({
|
expect(v.parse(registerAddressTransactionSchema, registerAddress)).toEqual({
|
||||||
user: {
|
user: {
|
||||||
communityTopicId: topic,
|
communityTopicId: topic,
|
||||||
|
communityId: communityUuid,
|
||||||
account: {
|
account: {
|
||||||
userUuid,
|
userUuid,
|
||||||
accountNr: 0,
|
accountNr: 0,
|
||||||
@ -101,10 +108,12 @@ describe('transaction schemas', () => {
|
|||||||
const gradidoTransfer: TransactionInput = {
|
const gradidoTransfer: TransactionInput = {
|
||||||
user: {
|
user: {
|
||||||
communityTopicId: topicString,
|
communityTopicId: topicString,
|
||||||
|
communityId: communityUuidString,
|
||||||
account: { userUuid: userUuidString },
|
account: { userUuid: userUuidString },
|
||||||
},
|
},
|
||||||
linkedUser: {
|
linkedUser: {
|
||||||
communityTopicId: topicString,
|
communityTopicId: topicString,
|
||||||
|
communityId: communityUuidString,
|
||||||
account: { userUuid: userUuidString },
|
account: { userUuid: userUuidString },
|
||||||
},
|
},
|
||||||
amount: '100',
|
amount: '100',
|
||||||
@ -115,6 +124,7 @@ describe('transaction schemas', () => {
|
|||||||
expect(v.parse(transactionSchema, gradidoTransfer)).toEqual({
|
expect(v.parse(transactionSchema, gradidoTransfer)).toEqual({
|
||||||
user: {
|
user: {
|
||||||
communityTopicId: topic,
|
communityTopicId: topic,
|
||||||
|
communityId: communityUuid,
|
||||||
account: {
|
account: {
|
||||||
userUuid,
|
userUuid,
|
||||||
accountNr: 0,
|
accountNr: 0,
|
||||||
@ -122,6 +132,7 @@ describe('transaction schemas', () => {
|
|||||||
},
|
},
|
||||||
linkedUser: {
|
linkedUser: {
|
||||||
communityTopicId: topic,
|
communityTopicId: topic,
|
||||||
|
communityId: communityUuid,
|
||||||
account: {
|
account: {
|
||||||
userUuid,
|
userUuid,
|
||||||
accountNr: 0,
|
accountNr: 0,
|
||||||
@ -138,10 +149,12 @@ describe('transaction schemas', () => {
|
|||||||
const gradidoCreation: TransactionInput = {
|
const gradidoCreation: TransactionInput = {
|
||||||
user: {
|
user: {
|
||||||
communityTopicId: topicString,
|
communityTopicId: topicString,
|
||||||
|
communityId: communityUuidString,
|
||||||
account: { userUuid: userUuidString },
|
account: { userUuid: userUuidString },
|
||||||
},
|
},
|
||||||
linkedUser: {
|
linkedUser: {
|
||||||
communityTopicId: topicString,
|
communityTopicId: topicString,
|
||||||
|
communityId: communityUuidString,
|
||||||
account: { userUuid: userUuidString },
|
account: { userUuid: userUuidString },
|
||||||
},
|
},
|
||||||
amount: '1000',
|
amount: '1000',
|
||||||
@ -153,10 +166,12 @@ describe('transaction schemas', () => {
|
|||||||
expect(v.parse(transactionSchema, gradidoCreation)).toEqual({
|
expect(v.parse(transactionSchema, gradidoCreation)).toEqual({
|
||||||
user: {
|
user: {
|
||||||
communityTopicId: topic,
|
communityTopicId: topic,
|
||||||
|
communityId: communityUuid,
|
||||||
account: { userUuid, accountNr: 0 },
|
account: { userUuid, accountNr: 0 },
|
||||||
},
|
},
|
||||||
linkedUser: {
|
linkedUser: {
|
||||||
communityTopicId: topic,
|
communityTopicId: topic,
|
||||||
|
communityId: communityUuid,
|
||||||
account: { userUuid, accountNr: 0 },
|
account: { userUuid, accountNr: 0 },
|
||||||
},
|
},
|
||||||
amount: v.parse(gradidoAmountSchema, gradidoCreation.amount!),
|
amount: v.parse(gradidoAmountSchema, gradidoCreation.amount!),
|
||||||
@ -172,12 +187,14 @@ describe('transaction schemas', () => {
|
|||||||
const gradidoTransactionLink: TransactionInput = {
|
const gradidoTransactionLink: TransactionInput = {
|
||||||
user: {
|
user: {
|
||||||
communityTopicId: topicString,
|
communityTopicId: topicString,
|
||||||
|
communityId: communityUuidString,
|
||||||
account: {
|
account: {
|
||||||
userUuid: userUuidString,
|
userUuid: userUuidString,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
linkedUser: {
|
linkedUser: {
|
||||||
communityTopicId: topicString,
|
communityTopicId: topicString,
|
||||||
|
communityId: communityUuidString,
|
||||||
seed,
|
seed,
|
||||||
},
|
},
|
||||||
amount: '100',
|
amount: '100',
|
||||||
@ -189,6 +206,7 @@ describe('transaction schemas', () => {
|
|||||||
expect(v.parse(transactionSchema, gradidoTransactionLink)).toEqual({
|
expect(v.parse(transactionSchema, gradidoTransactionLink)).toEqual({
|
||||||
user: {
|
user: {
|
||||||
communityTopicId: topic,
|
communityTopicId: topic,
|
||||||
|
communityId: communityUuid,
|
||||||
account: {
|
account: {
|
||||||
userUuid,
|
userUuid,
|
||||||
accountNr: 0,
|
accountNr: 0,
|
||||||
@ -196,6 +214,7 @@ describe('transaction schemas', () => {
|
|||||||
},
|
},
|
||||||
linkedUser: {
|
linkedUser: {
|
||||||
communityTopicId: topic,
|
communityTopicId: topic,
|
||||||
|
communityId: communityUuid,
|
||||||
seed: seedParsed,
|
seed: seedParsed,
|
||||||
},
|
},
|
||||||
amount: v.parse(gradidoAmountSchema, gradidoTransactionLink.amount!),
|
amount: v.parse(gradidoAmountSchema, gradidoTransactionLink.amount!),
|
||||||
|
|||||||
@ -43,12 +43,14 @@ export type Transaction = v.InferOutput<typeof transactionSchema>
|
|||||||
// if the account is identified by seed
|
// if the account is identified by seed
|
||||||
export const seedAccountSchema = v.object({
|
export const seedAccountSchema = v.object({
|
||||||
communityTopicId: hieroIdSchema,
|
communityTopicId: hieroIdSchema,
|
||||||
|
communityId: uuidv4Schema,
|
||||||
seed: identifierSeedSchema,
|
seed: identifierSeedSchema,
|
||||||
})
|
})
|
||||||
|
|
||||||
// if the account is identified by userUuid and accountNr
|
// if the account is identified by userUuid and accountNr
|
||||||
export const userAccountSchema = v.object({
|
export const userAccountSchema = v.object({
|
||||||
communityTopicId: hieroIdSchema,
|
communityTopicId: hieroIdSchema,
|
||||||
|
communityId: uuidv4Schema,
|
||||||
account: identifierCommunityAccountSchema,
|
account: identifierCommunityAccountSchema,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
import { describe, expect, it } from 'bun:test'
|
import { describe, expect, it } from 'bun:test'
|
||||||
import { TypeCompiler } from '@sinclair/typebox/compiler'
|
import { TypeCompiler } from '@sinclair/typebox/compiler'
|
||||||
import { Static, TypeBoxFromValibot } from '@sinclair/typemap'
|
import { Static, TypeBoxFromValibot } from '@sinclair/typemap'
|
||||||
import { AddressType_COMMUNITY_AUF } from 'gradido-blockchain-js'
|
import { AddressType_COMMUNITY_AUF, InMemoryBlockchainProvider } from 'gradido-blockchain-js'
|
||||||
import * as v from 'valibot'
|
import * as v from 'valibot'
|
||||||
import { AccountType } from '../data/AccountType.enum'
|
import { AccountType } from '../data/AccountType.enum'
|
||||||
import {
|
import {
|
||||||
@ -96,12 +96,15 @@ describe('basic.schema', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('confirmedTransactionSchema', () => {
|
it('confirmedTransactionSchema', () => {
|
||||||
const confirmedTransaction = v.parse(
|
// create blockchain in native module
|
||||||
confirmedTransactionSchema,
|
const communityId = 'fcd48487-6d31-4f4c-be9b-b3c8ca853912'
|
||||||
'CAcS5AEKZgpkCiCBZwMplGmI7fRR9MQkaR2Dz1qQQ5BCiC1btyJD71Ue9BJABODQ9sS70th9yHn8X3K+SNv2gsiIdX/V09baCvQCb+yEj2Dd/fzShIYqf3pooIMwJ01BkDJdNGBZs5MDzEAkChJ6ChkIAhIVRGFua2UgZnVlciBkZWluIFNlaW4hEggIgMy5/wUQABoDMy41IAAyTAooCiDbDtYSWhTwMKvtG/yDHgohjPn6v87n7NWBwMDniPAXxxCUmD0aABIgJE0o18xb6P6PsNjh0bkN52AzhggteTzoh09jV+blMq0aCAjC8rn/BRAAIgMzLjUqICiljeEjGHifWe4VNzoe+DN9oOLNZvJmv3VlkP+1RH7MMiAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADomCiDbDtYSWhTwMKvtG/yDHgohjPn6v87n7NWBwMDniPAXxxDAhD06JwogJE0o18xb6P6PsNjh0bkN52AzhggteTzoh09jV+blMq0Q65SlBA==',
|
InMemoryBlockchainProvider.getInstance().getBlockchain(communityId)
|
||||||
)
|
const confirmedTransaction = v.parse(confirmedTransactionSchema, {
|
||||||
|
base64:
|
||||||
|
'CAcS4AEKZgpkCiCBZwMplGmI7fRR9MQkaR2Dz1qQQ5BCiC1btyJD71Ue9BJABODQ9sS70th9yHn8X3K+SNv2gsiIdX/V09baCvQCb+zo7nEQgCUXOEe/tN7YaRppwt6TDcXBPxkwnw4gfpCODhJ0ChkIAhIVRGFua2UgZnVlciBkZWluIFNlaW4hEgYIgMy5/wUaAzMuNTJKCiYKINsO1hJaFPAwq+0b/IMeCiGM+fq/zufs1YHAwOeI8BfHEJSYPRIgJE0o18xb6P6PsNjh0bkN52AzhggteTzoh09jV+blMq0aABoGCMLyuf8FIgMzLjcqIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMhUIAhoRCgkIqemnUhD+4wESBBj8sgc6Jgog2w7WEloU8DCr7Rv8gx4KIYz5+r/O5+zVgcDA54jwF8cQwIQ9OicKICRNKNfMW+j+j7DY4dG5DedgM4YILXk86IdPY1fm5TKtEOuUpQRAAg==',
|
||||||
|
communityId,
|
||||||
|
})
|
||||||
expect(confirmedTransaction.getId()).toBe(7)
|
expect(confirmedTransaction.getId()).toBe(7)
|
||||||
expect(confirmedTransaction.getConfirmedAt().getSeconds()).toBe(1609464130)
|
expect(confirmedTransaction.getConfirmedAt().getSeconds()).toBe(1609464130)
|
||||||
expect(confirmedTransaction.getVersionNumber()).toBe('3.5')
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import {
|
|||||||
toAccountType,
|
toAccountType,
|
||||||
toAddressType,
|
toAddressType,
|
||||||
} from '../utils/typeConverter'
|
} from '../utils/typeConverter'
|
||||||
|
import { Uuidv4, uuidv4Schema } from './typeGuard.schema'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* dateSchema for creating a date from string or Date object
|
* dateSchema for creating a date from string or Date object
|
||||||
@ -72,17 +73,23 @@ export const accountTypeSchema = v.pipe(
|
|||||||
export const confirmedTransactionSchema = v.pipe(
|
export const confirmedTransactionSchema = v.pipe(
|
||||||
v.union([
|
v.union([
|
||||||
v.instance(ConfirmedTransaction, 'expect ConfirmedTransaction'),
|
v.instance(ConfirmedTransaction, 'expect ConfirmedTransaction'),
|
||||||
v.pipe(
|
v.object({
|
||||||
v.string('expect confirmed Transaction base64 as string type'),
|
base64: v.pipe(
|
||||||
v.base64('expect to be valid base64'),
|
v.string('expect confirmed Transaction base64 as string type'),
|
||||||
),
|
v.base64('expect to be valid base64'),
|
||||||
|
),
|
||||||
|
communityId: uuidv4Schema,
|
||||||
|
}),
|
||||||
]),
|
]),
|
||||||
v.transform<string | ConfirmedTransaction, ConfirmedTransaction>(
|
v.transform<ConfirmedTransaction | { base64: string; communityId: Uuidv4 }, ConfirmedTransaction>(
|
||||||
(data: string | ConfirmedTransaction) => {
|
(data: string | ConfirmedTransaction | { base64: string; communityId: Uuidv4 }) => {
|
||||||
if (data instanceof ConfirmedTransaction) {
|
if (data instanceof ConfirmedTransaction) {
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
return confirmedTransactionFromBase64(data)
|
if (typeof data === 'object' && 'base64' in data && 'communityId' in data) {
|
||||||
|
return confirmedTransactionFromBase64(data.base64, data.communityId)
|
||||||
|
}
|
||||||
|
throw new Error("invalid data, community id missing, couldn't deserialize")
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { describe, expect, it } from 'bun:test'
|
import { describe, expect, it } from 'bun:test'
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
import * as v from 'valibot'
|
import * as v from 'valibot'
|
||||||
import { memoSchema, uuidv4Schema } from './typeGuard.schema'
|
import { MEMO_MAX_CHARS, MEMO_MIN_CHARS, memoSchema, uuidv4Schema } from './typeGuard.schema'
|
||||||
|
|
||||||
describe('typeGuard.schema', () => {
|
describe('typeGuard.schema', () => {
|
||||||
describe('Uuidv4', () => {
|
describe('Uuidv4', () => {
|
||||||
@ -20,18 +20,20 @@ describe('typeGuard.schema', () => {
|
|||||||
expect(memoValueParsed.toString()).toBe(memoValue)
|
expect(memoValueParsed.toString()).toBe(memoValue)
|
||||||
})
|
})
|
||||||
it('max length', () => {
|
it('max length', () => {
|
||||||
const memoValue = 's'.repeat(255)
|
const memoValue = 's'.repeat(MEMO_MAX_CHARS)
|
||||||
const memoValueParsed = v.parse(memoSchema, memoValue)
|
const memoValueParsed = v.parse(memoSchema, memoValue)
|
||||||
expect(memoValueParsed.toString()).toBe(memoValue)
|
expect(memoValueParsed.toString()).toBe(memoValue)
|
||||||
})
|
})
|
||||||
it('to short', () => {
|
it('to short', () => {
|
||||||
const memoValue = 'memo'
|
const memoValue = 'memo'
|
||||||
expect(() => v.parse(memoSchema, memoValue)).toThrow(new Error('expect string length >= 5'))
|
expect(() => v.parse(memoSchema, memoValue)).toThrow(
|
||||||
|
new Error(`expect string length >= ${MEMO_MIN_CHARS}`),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
it('to long', () => {
|
it('to long', () => {
|
||||||
const memoValue = 's'.repeat(256)
|
const memoValue = 's'.repeat(MEMO_MAX_CHARS + 1)
|
||||||
expect(() => v.parse(memoSchema, memoValue)).toThrow(
|
expect(() => v.parse(memoSchema, memoValue)).toThrow(
|
||||||
new Error('expect string length <= 255'),
|
new Error(`expect string length <= ${MEMO_MAX_CHARS}`),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -163,7 +163,7 @@ export type HieroTransactionIdInput = v.InferInput<typeof hieroTransactionIdStri
|
|||||||
* memo string inside bounds [5, 255]
|
* memo string inside bounds [5, 255]
|
||||||
*/
|
*/
|
||||||
export const MEMO_MIN_CHARS = 5
|
export const MEMO_MIN_CHARS = 5
|
||||||
export const MEMO_MAX_CHARS = 255
|
export const MEMO_MAX_CHARS = 512
|
||||||
|
|
||||||
declare const validMemo: unique symbol
|
declare const validMemo: unique symbol
|
||||||
export type Memo = string & { [validMemo]: true }
|
export type Memo = string & { [validMemo]: true }
|
||||||
|
|||||||
@ -1,6 +1,11 @@
|
|||||||
import { beforeAll, describe, expect, it, mock } from 'bun:test'
|
import { beforeAll, describe, expect, it, mock } from 'bun:test'
|
||||||
import { AccountId, Timestamp, TransactionId } from '@hashgraph/sdk'
|
import { AccountId, Timestamp, TransactionId } from '@hashgraph/sdk'
|
||||||
import { GradidoTransaction, KeyPairEd25519, MemoryBlock } from 'gradido-blockchain-js'
|
import {
|
||||||
|
GradidoTransaction,
|
||||||
|
InMemoryBlockchainProvider,
|
||||||
|
KeyPairEd25519,
|
||||||
|
MemoryBlock,
|
||||||
|
} from 'gradido-blockchain-js'
|
||||||
import * as v from 'valibot'
|
import * as v from 'valibot'
|
||||||
import { KeyPairCacheManager } from '../cache/KeyPairCacheManager'
|
import { KeyPairCacheManager } from '../cache/KeyPairCacheManager'
|
||||||
import { HieroId, hieroIdSchema } from '../schemas/typeGuard.schema'
|
import { HieroId, hieroIdSchema } from '../schemas/typeGuard.schema'
|
||||||
@ -55,9 +60,13 @@ beforeAll(() => {
|
|||||||
|
|
||||||
describe('Server', () => {
|
describe('Server', () => {
|
||||||
it('send register address transaction', async () => {
|
it('send register address transaction', async () => {
|
||||||
|
// create blockchain in native module
|
||||||
|
const communityId = '1e88a0f4-d4fc-4cae-a7e8-a88e613ce324'
|
||||||
|
InMemoryBlockchainProvider.getInstance().getBlockchain(communityId)
|
||||||
const transaction = {
|
const transaction = {
|
||||||
user: {
|
user: {
|
||||||
communityTopicId: '0.0.21732',
|
communityTopicId: '0.0.21732',
|
||||||
|
communityId,
|
||||||
account: {
|
account: {
|
||||||
userUuid,
|
userUuid,
|
||||||
accountNr: 0,
|
accountNr: 0,
|
||||||
|
|||||||
@ -69,9 +69,10 @@ export const appRoutes = new Elysia()
|
|||||||
// check if account exists by user, call example:
|
// check if account exists by user, call example:
|
||||||
// GET /isAccountExist/by-user/0.0.21732/408780b2-59b3-402a-94be-56a4f4f4e8ec/0
|
// GET /isAccountExist/by-user/0.0.21732/408780b2-59b3-402a-94be-56a4f4f4e8ec/0
|
||||||
.get(
|
.get(
|
||||||
'/isAccountExist/by-user/:communityTopicId/:userUuid/:accountNr',
|
'/isAccountExist/by-user/:communityId/:communityTopicId/:userUuid/:accountNr',
|
||||||
async ({ params: { communityTopicId, userUuid, accountNr } }) => ({
|
async ({ params: { communityId, communityTopicId, userUuid, accountNr } }) => ({
|
||||||
exists: await isAccountExist({
|
exists: await isAccountExist({
|
||||||
|
communityId,
|
||||||
communityTopicId,
|
communityTopicId,
|
||||||
account: { userUuid, accountNr },
|
account: { userUuid, accountNr },
|
||||||
}),
|
}),
|
||||||
@ -84,9 +85,10 @@ export const appRoutes = new Elysia()
|
|||||||
// check if account exists by seed, call example:
|
// check if account exists by seed, call example:
|
||||||
// GET /isAccountExist/by-seed/0.0.21732/0c4676adfd96519a0551596c
|
// GET /isAccountExist/by-seed/0.0.21732/0c4676adfd96519a0551596c
|
||||||
.get(
|
.get(
|
||||||
'/isAccountExist/by-seed/:communityTopicId/:seed',
|
'/isAccountExist/by-seed/:communityId/:communityTopicId/:seed',
|
||||||
async ({ params: { communityTopicId, seed } }) => ({
|
async ({ params: { communityId, communityTopicId, seed } }) => ({
|
||||||
exists: await isAccountExist({
|
exists: await isAccountExist({
|
||||||
|
communityId,
|
||||||
communityTopicId,
|
communityTopicId,
|
||||||
seed,
|
seed,
|
||||||
}),
|
}),
|
||||||
@ -145,7 +147,7 @@ async function isAccountExist(identifierAccount: IdentifierAccountInput): Promis
|
|||||||
// ask gradido node server for account type, if type !== NONE account exist
|
// ask gradido node server for account type, if type !== NONE account exist
|
||||||
const addressType = await GradidoNodeClient.getInstance().getAddressType(
|
const addressType = await GradidoNodeClient.getInstance().getAddressType(
|
||||||
publicKey.convertToHex(),
|
publicKey.convertToHex(),
|
||||||
identifierAccountParsed.communityTopicId,
|
identifierAccountParsed.communityId,
|
||||||
)
|
)
|
||||||
const exists = addressType !== AddressType_NONE
|
const exists = addressType !== AddressType_NONE
|
||||||
const endTime = Date.now()
|
const endTime = Date.now()
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { t } from 'elysia'
|
|||||||
import { hieroIdSchema, uuidv4Schema } from '../schemas/typeGuard.schema'
|
import { hieroIdSchema, uuidv4Schema } from '../schemas/typeGuard.schema'
|
||||||
|
|
||||||
export const accountIdentifierUserTypeBoxSchema = t.Object({
|
export const accountIdentifierUserTypeBoxSchema = t.Object({
|
||||||
|
communityId: TypeBoxFromValibot(uuidv4Schema),
|
||||||
communityTopicId: TypeBoxFromValibot(hieroIdSchema),
|
communityTopicId: TypeBoxFromValibot(hieroIdSchema),
|
||||||
userUuid: TypeBoxFromValibot(uuidv4Schema),
|
userUuid: TypeBoxFromValibot(uuidv4Schema),
|
||||||
accountNr: t.Number({ min: 0 }),
|
accountNr: t.Number({ min: 0 }),
|
||||||
@ -10,6 +11,7 @@ export const accountIdentifierUserTypeBoxSchema = t.Object({
|
|||||||
|
|
||||||
// identifier for a gradido account created by transaction link / deferred transfer
|
// identifier for a gradido account created by transaction link / deferred transfer
|
||||||
export const accountIdentifierSeedTypeBoxSchema = t.Object({
|
export const accountIdentifierSeedTypeBoxSchema = t.Object({
|
||||||
|
communityId: TypeBoxFromValibot(uuidv4Schema),
|
||||||
communityTopicId: TypeBoxFromValibot(hieroIdSchema),
|
communityTopicId: TypeBoxFromValibot(hieroIdSchema),
|
||||||
seed: TypeBoxFromValibot(uuidv4Schema),
|
seed: TypeBoxFromValibot(uuidv4Schema),
|
||||||
})
|
})
|
||||||
|
|||||||
@ -9,7 +9,7 @@ export function checkFileExist(filePath: string): boolean {
|
|||||||
fs.accessSync(filePath, fs.constants.R_OK | fs.constants.W_OK)
|
fs.accessSync(filePath, fs.constants.R_OK | fs.constants.W_OK)
|
||||||
return true
|
return true
|
||||||
} catch (_err) {
|
} catch (_err) {
|
||||||
// logger.debug(`file ${filePath} does not exist: ${_err}`)
|
logger.debug(`file ${filePath} does not exist: ${_err}`)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -28,3 +28,7 @@ export function checkPathExist(path: string, createIfMissing: boolean = false):
|
|||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function toFolderName(name: string): string {
|
||||||
|
return name.toLowerCase().replace(/[^a-z0-9]/g, '_')
|
||||||
|
}
|
||||||
|
|||||||
@ -29,7 +29,7 @@ export async function isPortOpen(
|
|||||||
socket.destroy()
|
socket.destroy()
|
||||||
const logger = getLogger(`${LOG4JS_BASE_CATEGORY}.network.isPortOpen`)
|
const logger = getLogger(`${LOG4JS_BASE_CATEGORY}.network.isPortOpen`)
|
||||||
logger.addContext('url', url)
|
logger.addContext('url', url)
|
||||||
logger.error(`${err.message}: ${err.code}`)
|
logger.debug(`${err.message}: ${err.code}`)
|
||||||
resolve(false)
|
resolve(false)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -6,14 +6,18 @@ import {
|
|||||||
} from 'gradido-blockchain-js'
|
} from 'gradido-blockchain-js'
|
||||||
import { AccountType } from '../data/AccountType.enum'
|
import { AccountType } from '../data/AccountType.enum'
|
||||||
import { AddressType } from '../data/AddressType.enum'
|
import { AddressType } from '../data/AddressType.enum'
|
||||||
|
import { Uuidv4 } from '../schemas/typeGuard.schema'
|
||||||
|
|
||||||
export const confirmedTransactionFromBase64 = (base64: string): ConfirmedTransaction => {
|
export const confirmedTransactionFromBase64 = (
|
||||||
|
base64: string,
|
||||||
|
communityId: Uuidv4,
|
||||||
|
): ConfirmedTransaction => {
|
||||||
const confirmedTransactionBinaryPtr = MemoryBlock.createPtr(MemoryBlock.fromBase64(base64))
|
const confirmedTransactionBinaryPtr = MemoryBlock.createPtr(MemoryBlock.fromBase64(base64))
|
||||||
const deserializer = new InteractionDeserialize(
|
const deserializer = new InteractionDeserialize(
|
||||||
confirmedTransactionBinaryPtr,
|
confirmedTransactionBinaryPtr,
|
||||||
DeserializeType_CONFIRMED_TRANSACTION,
|
DeserializeType_CONFIRMED_TRANSACTION,
|
||||||
)
|
)
|
||||||
deserializer.run()
|
deserializer.run(communityId)
|
||||||
const confirmedTransaction = deserializer.getConfirmedTransaction()
|
const confirmedTransaction = deserializer.getConfirmedTransaction()
|
||||||
if (!confirmedTransaction) {
|
if (!confirmedTransaction) {
|
||||||
throw new Error("invalid data, couldn't deserialize")
|
throw new Error("invalid data, couldn't deserialize")
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
Subproject commit 0c14b7eea29b8911cbe3cb303f5b0b61ce9bf6f4
|
Subproject commit e1d13e3336199eae615557d11d6671c034860326
|
||||||
@ -34,7 +34,7 @@
|
|||||||
"auto-changelog": "^2.4.0",
|
"auto-changelog": "^2.4.0",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"jose": "^4.14.4",
|
"jose": "^4.14.4",
|
||||||
"turbo": "^2.5.0",
|
"turbo": "^2.8.12",
|
||||||
"uuid": "^8.3.2"
|
"uuid": "^8.3.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@ -1,4 +1,7 @@
|
|||||||
|
import Decimal from 'decimal.js-light'
|
||||||
|
|
||||||
export const DECAY_START_TIME = new Date('2021-05-13T17:46:31Z')
|
export const DECAY_START_TIME = new Date('2021-05-13T17:46:31Z')
|
||||||
|
export const DECAY_FACTOR = new Decimal('0.99999997803504048973201202316767079413460520837376')
|
||||||
export const SECONDS_PER_YEAR_GREGORIAN_CALENDER = 31556952.0
|
export const SECONDS_PER_YEAR_GREGORIAN_CALENDER = 31556952.0
|
||||||
export const LOG4JS_BASE_CATEGORY_NAME = 'shared'
|
export const LOG4JS_BASE_CATEGORY_NAME = 'shared'
|
||||||
export const REDEEM_JWT_TOKEN_EXPIRATION = '10m'
|
export const REDEEM_JWT_TOKEN_EXPIRATION = '10m'
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { Decimal } from 'decimal.js-light'
|
import { Decimal } from 'decimal.js-light'
|
||||||
|
|
||||||
import { DECAY_START_TIME, SECONDS_PER_YEAR_GREGORIAN_CALENDER } from '../const'
|
import { DECAY_FACTOR, DECAY_START_TIME, SECONDS_PER_YEAR_GREGORIAN_CALENDER } from '../const'
|
||||||
|
|
||||||
Decimal.set({
|
Decimal.set({
|
||||||
precision: 25,
|
precision: 25,
|
||||||
@ -16,21 +16,28 @@ export interface Decay {
|
|||||||
duration: number | null
|
duration: number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// legacy decay formula
|
||||||
export function decayFormula(value: Decimal, seconds: number): Decimal {
|
export function decayFormula(value: Decimal, seconds: number): Decimal {
|
||||||
// TODO why do we need to convert this here to a string to work properly?
|
// TODO why do we need to convert this here to a string to work properly?
|
||||||
// chatgpt: We convert to string here to avoid precision loss:
|
// chatgpt: We convert to string here to avoid precision loss:
|
||||||
// .pow(seconds) can internally round the result, especially for large values of `seconds`.
|
// .pow(seconds) can internally round the result, especially for large values of `seconds`.
|
||||||
// Using .toString() ensures full precision is preserved in the multiplication.
|
// Using .toString() ensures full precision is preserved in the multiplication.
|
||||||
return value.mul(
|
return value.mul(DECAY_FACTOR.pow(seconds).toString())
|
||||||
new Decimal('0.99999997803504048973201202316767079413460520837376').pow(seconds).toString(),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// legacy reverse decay formula
|
||||||
|
export function reverseLegacyDecay(result: Decimal, seconds: number): Decimal {
|
||||||
|
return result.div(DECAY_FACTOR.pow(seconds).toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
// fast and more correct decay formula
|
||||||
export function decayFormulaFast(value: Decimal, seconds: number): Decimal {
|
export function decayFormulaFast(value: Decimal, seconds: number): Decimal {
|
||||||
return value.mul(
|
return value.mul(
|
||||||
new Decimal(2).pow(new Decimal(-seconds).div(new Decimal(SECONDS_PER_YEAR_GREGORIAN_CALENDER))),
|
new Decimal(2).pow(new Decimal(-seconds).div(new Decimal(SECONDS_PER_YEAR_GREGORIAN_CALENDER))),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// compound interest formula, the reverse decay formula for decayFormulaFast
|
||||||
export function compoundInterest(value: Decimal, seconds: number): Decimal {
|
export function compoundInterest(value: Decimal, seconds: number): Decimal {
|
||||||
return value.mul(
|
return value.mul(
|
||||||
new Decimal(2).pow(new Decimal(seconds).div(new Decimal(SECONDS_PER_YEAR_GREGORIAN_CALENDER))),
|
new Decimal(2).pow(new Decimal(seconds).div(new Decimal(SECONDS_PER_YEAR_GREGORIAN_CALENDER))),
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user