mirror of
https://github.com/IT4Change/gradido.git
synced 2026-02-06 09:56:05 +00:00
Merge branch 'dlt_deferred_transfer'
This commit is contained in:
commit
df28a5b4d1
9
.github/workflows/test_dlt_connector.yml
vendored
9
.github/workflows/test_dlt_connector.yml
vendored
@ -60,13 +60,6 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: DLT-Connector | docker-compose mariadb
|
||||
run: docker compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps mariadb
|
||||
|
||||
- name: Sleep for 30 seconds
|
||||
run: sleep 30s
|
||||
shell: bash
|
||||
|
||||
- name: DLT-Connector | Unit tests
|
||||
run: cd dlt-database && yarn && yarn build && cd ../dlt-connector && yarn && yarn test
|
||||
run: cd dlt-connector && yarn && yarn test
|
||||
|
||||
@ -1,36 +1,31 @@
|
||||
import { Transaction as DbTransaction } from 'database'
|
||||
import { GraphQLClient, gql } from 'graphql-request'
|
||||
|
||||
import { CONFIG } from '@/config'
|
||||
import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const'
|
||||
import { TransactionTypeId } from '@/graphql/enum/TransactionTypeId'
|
||||
import { LogError } from '@/server/LogError'
|
||||
import { getLogger } from 'log4js'
|
||||
|
||||
import { TransactionDraft } from './model/TransactionDraft'
|
||||
import { TransactionResult } from './model/TransactionResult'
|
||||
import { UserIdentifier } from './model/UserIdentifier'
|
||||
|
||||
const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.apis.dltConnector`)
|
||||
|
||||
const sendTransaction = gql`
|
||||
mutation ($input: TransactionInput!) {
|
||||
mutation ($input: TransactionDraft!) {
|
||||
sendTransaction(data: $input) {
|
||||
dltTransactionIdHex
|
||||
error {
|
||||
message
|
||||
name
|
||||
}
|
||||
succeed
|
||||
recipe {
|
||||
createdAt
|
||||
type
|
||||
messageIdHex
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
// from ChatGPT
|
||||
function getTransactionTypeString(id: TransactionTypeId): string {
|
||||
const key = Object.keys(TransactionTypeId).find(
|
||||
(key) => TransactionTypeId[key as keyof typeof TransactionTypeId] === id,
|
||||
)
|
||||
if (key === undefined) {
|
||||
throw new LogError('invalid transaction type id: ' + id.toString())
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
// Source: https://refactoring.guru/design-patterns/singleton/typescript/example
|
||||
// and ../federation/client/FederationClientFactory.ts
|
||||
/**
|
||||
@ -65,7 +60,10 @@ export class DltConnectorClient {
|
||||
if (!DltConnectorClient.instance.client) {
|
||||
try {
|
||||
DltConnectorClient.instance.client = new GraphQLClient(CONFIG.DLT_CONNECTOR_URL, {
|
||||
method: 'GET',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
jsonSerializer: {
|
||||
parse: JSON.parse,
|
||||
stringify: JSON.stringify,
|
||||
@ -83,44 +81,13 @@ export class DltConnectorClient {
|
||||
* transmit transaction via dlt-connector to iota
|
||||
* and update dltTransactionId of transaction in db with iota message id
|
||||
*/
|
||||
public async transmitTransaction(transaction: DbTransaction): Promise<boolean> {
|
||||
const typeString = getTransactionTypeString(transaction.typeId)
|
||||
// no negative values in dlt connector, gradido concept don't use negative values so the code don't use it too
|
||||
const amountString = transaction.amount.abs().toString()
|
||||
const params = {
|
||||
input: {
|
||||
user: {
|
||||
uuid: transaction.userGradidoID,
|
||||
communityUuid: transaction.userCommunityUuid,
|
||||
} as UserIdentifier,
|
||||
linkedUser: {
|
||||
uuid: transaction.linkedUserGradidoID,
|
||||
communityUuid: transaction.linkedUserCommunityUuid,
|
||||
} as UserIdentifier,
|
||||
amount: amountString,
|
||||
type: typeString,
|
||||
createdAt: transaction.balanceDate.toISOString(),
|
||||
backendTransactionId: transaction.id,
|
||||
targetDate: transaction.creationDate?.toISOString(),
|
||||
},
|
||||
}
|
||||
try {
|
||||
// TODO: add account nr for user after they have also more than one account in backend
|
||||
logger.debug('transmit transaction to dlt connector', params)
|
||||
const {
|
||||
data: {
|
||||
sendTransaction: { error, succeed },
|
||||
},
|
||||
} = await this.client.rawRequest<{ sendTransaction: TransactionResult }>(
|
||||
sendTransaction,
|
||||
params,
|
||||
)
|
||||
if (error) {
|
||||
throw new Error(error.message)
|
||||
}
|
||||
return succeed
|
||||
} catch (e) {
|
||||
throw new LogError('Error send sending transaction to dlt-connector: ', e)
|
||||
}
|
||||
public async sendTransaction(input: TransactionDraft): Promise<TransactionResult | undefined> {
|
||||
logger.debug('transmit transaction or user to dlt connector', input)
|
||||
const {
|
||||
data: { sendTransaction: result },
|
||||
} = await this.client.rawRequest<{ sendTransaction: TransactionResult }>(sendTransaction, {
|
||||
input,
|
||||
})
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
9
backend/src/apis/dltConnector/enum/AccountType.ts
Normal file
9
backend/src/apis/dltConnector/enum/AccountType.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export enum AccountType {
|
||||
NONE = 'NONE', // if no address was found
|
||||
COMMUNITY_HUMAN = 'COMMUNITY_HUMAN', // creation account for human
|
||||
COMMUNITY_GMW = 'COMMUNITY_GMW', // community public budget account
|
||||
COMMUNITY_AUF = 'COMMUNITY_AUF', // community compensation and environment founds account
|
||||
COMMUNITY_PROJECT = 'COMMUNITY_PROJECT', // no creations allowed
|
||||
SUBACCOUNT = 'SUBACCOUNT', // no creations allowed
|
||||
CRYPTO_ACCOUNT = 'CRYPTO_ACCOUNT', // user control his keys, no creations
|
||||
}
|
||||
9
backend/src/apis/dltConnector/enum/DltTransactionType.ts
Normal file
9
backend/src/apis/dltConnector/enum/DltTransactionType.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export enum DltTransactionType {
|
||||
UNKNOWN = 0,
|
||||
REGISTER_ADDRESS = 1,
|
||||
CREATION = 2,
|
||||
TRANSFER = 3,
|
||||
DEFERRED_TRANSFER = 4,
|
||||
REDEEM_DEFERRED_TRANSFER = 5,
|
||||
DELETE_DEFERRED_TRANSFER = 6,
|
||||
}
|
||||
@ -1,11 +1,19 @@
|
||||
import { registerEnumType } from 'type-graphql'
|
||||
|
||||
/**
|
||||
* Transaction Types on Blockchain
|
||||
*/
|
||||
export enum TransactionType {
|
||||
GRADIDO_TRANSFER = 1,
|
||||
GRADIDO_CREATION = 2,
|
||||
GROUP_FRIENDS_UPDATE = 3,
|
||||
REGISTER_ADDRESS = 4,
|
||||
GRADIDO_DEFERRED_TRANSFER = 5,
|
||||
COMMUNITY_ROOT = 6,
|
||||
GRADIDO_TRANSFER = 'GRADIDO_TRANSFER',
|
||||
GRADIDO_CREATION = 'GRADIDO_CREATION',
|
||||
GROUP_FRIENDS_UPDATE = 'GROUP_FRIENDS_UPDATE',
|
||||
REGISTER_ADDRESS = 'REGISTER_ADDRESS',
|
||||
GRADIDO_DEFERRED_TRANSFER = 'GRADIDO_DEFERRED_TRANSFER',
|
||||
GRADIDO_REDEEM_DEFERRED_TRANSFER = 'GRADIDO_REDEEM_DEFERRED_TRANSFER',
|
||||
COMMUNITY_ROOT = 'COMMUNITY_ROOT',
|
||||
}
|
||||
|
||||
registerEnumType(TransactionType, {
|
||||
name: 'TransactionType', // this one is mandatory
|
||||
description: 'Type of the transaction', // this one is optional
|
||||
})
|
||||
|
||||
@ -0,0 +1,61 @@
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import { ObjectLiteral, OrderByCondition, SelectQueryBuilder } from '@dbTools/typeorm'
|
||||
import { DltTransaction } from '@entity/DltTransaction'
|
||||
|
||||
import { TransactionDraft } from '@dltConnector/model/TransactionDraft'
|
||||
|
||||
import { backendLogger as logger } from '@/server/logger'
|
||||
|
||||
export abstract class AbstractTransactionToDltRole<T extends ObjectLiteral> {
|
||||
protected self: T | null
|
||||
|
||||
// public interface
|
||||
public abstract initWithLast(): Promise<this>
|
||||
public abstract getTimestamp(): number
|
||||
public abstract convertToGraphqlInput(): TransactionDraft
|
||||
|
||||
public getEntity(): T | null {
|
||||
return this.self
|
||||
}
|
||||
|
||||
public async saveTransactionResult(messageId: string, error: string | null): Promise<void> {
|
||||
const dltTransaction = DltTransaction.create()
|
||||
dltTransaction.messageId = messageId
|
||||
dltTransaction.error = error
|
||||
this.setJoinIdAndType(dltTransaction)
|
||||
await DltTransaction.save(dltTransaction)
|
||||
if (dltTransaction.error) {
|
||||
logger.error(
|
||||
`Store dltTransaction with error: id=${dltTransaction.id}, error=${dltTransaction.error}`,
|
||||
)
|
||||
} else {
|
||||
logger.info(
|
||||
`Store dltTransaction: messageId=${dltTransaction.messageId}, id=${dltTransaction.id}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// intern
|
||||
protected abstract setJoinIdAndType(dltTransaction: DltTransaction): void
|
||||
|
||||
// helper
|
||||
protected createQueryForPendingItems(
|
||||
qb: SelectQueryBuilder<T>,
|
||||
joinCondition: string,
|
||||
orderBy: OrderByCondition,
|
||||
): SelectQueryBuilder<T> {
|
||||
return qb
|
||||
.leftJoin(DltTransaction, 'dltTransaction', joinCondition)
|
||||
.where('dltTransaction.user_id IS NULL')
|
||||
.andWhere('dltTransaction.transaction_id IS NULL')
|
||||
.andWhere('dltTransaction.transaction_link_Id IS NULL')
|
||||
.orderBy(orderBy)
|
||||
}
|
||||
|
||||
protected createDltTransactionEntry(messageId: string, error: string | null): DltTransaction {
|
||||
const dltTransaction = DltTransaction.create()
|
||||
dltTransaction.messageId = messageId
|
||||
dltTransaction.error = error
|
||||
return dltTransaction
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,85 @@
|
||||
import { DltTransaction } from '@entity/DltTransaction'
|
||||
import { TransactionLink } from '@entity/TransactionLink'
|
||||
|
||||
import { DltTransactionType } from '@dltConnector/enum/DltTransactionType'
|
||||
import { TransactionType } from '@dltConnector/enum/TransactionType'
|
||||
import { CommunityUser } from '@dltConnector/model/CommunityUser'
|
||||
import { IdentifierSeed } from '@dltConnector/model/IdentifierSeed'
|
||||
import { TransactionDraft } from '@dltConnector/model/TransactionDraft'
|
||||
import { UserIdentifier } from '@dltConnector/model/UserIdentifier'
|
||||
|
||||
import { LogError } from '@/server/LogError'
|
||||
|
||||
import { AbstractTransactionToDltRole } from './AbstractTransactionToDlt.role'
|
||||
|
||||
/**
|
||||
* redeem deferred transfer transaction by creator, so "deleting" it
|
||||
*/
|
||||
export class TransactionLinkDeleteToDltRole extends AbstractTransactionToDltRole<TransactionLink> {
|
||||
async initWithLast(): Promise<this> {
|
||||
const queryBuilder = this.createQueryForPendingItems(
|
||||
TransactionLink.createQueryBuilder().leftJoinAndSelect('TransactionLink.user', 'user'),
|
||||
'TransactionLink.id = dltTransaction.transactionLinkId and dltTransaction.type_id <> 4',
|
||||
// eslint-disable-next-line camelcase
|
||||
{ TransactionLink_deletedAt: 'ASC', User_id: 'ASC' },
|
||||
)
|
||||
.andWhere('TransactionLink.deletedAt IS NOT NULL')
|
||||
.withDeleted()
|
||||
/*
|
||||
const queryBuilder2 = TransactionLink.createQueryBuilder()
|
||||
.leftJoinAndSelect('TransactionLink.user', 'user')
|
||||
.where('TransactionLink.deletedAt IS NOT NULL')
|
||||
.andWhere(() => {
|
||||
const subQuery = DltTransaction.createQueryBuilder()
|
||||
.select('1')
|
||||
.where('DltTransaction.transaction_link_id = TransactionLink.id')
|
||||
.andWhere('DltTransaction.type_id = :typeId', {
|
||||
typeId: DltTransactionType.DELETE_DEFERRED_TRANSFER,
|
||||
})
|
||||
.getQuery()
|
||||
return `NOT EXIST (${subQuery})`
|
||||
})
|
||||
.withDeleted()
|
||||
// eslint-disable-next-line camelcase
|
||||
.orderBy({ TransactionLink_deletedAt: 'ASC', User_id: 'ASC' })
|
||||
*/
|
||||
// console.log('query: ', queryBuilder.getSql())
|
||||
this.self = await queryBuilder.getOne()
|
||||
return this
|
||||
}
|
||||
|
||||
public getTimestamp(): number {
|
||||
if (!this.self) {
|
||||
return Infinity
|
||||
}
|
||||
if (!this.self.deletedAt) {
|
||||
throw new LogError('not deleted transaction link selected')
|
||||
}
|
||||
return this.self.deletedAt.getTime()
|
||||
}
|
||||
|
||||
public convertToGraphqlInput(): TransactionDraft {
|
||||
if (!this.self) {
|
||||
throw new LogError('try to create dlt entry for empty transaction link')
|
||||
}
|
||||
if (!this.self.deletedAt) {
|
||||
throw new LogError('not deleted transaction link selected')
|
||||
}
|
||||
const draft = new TransactionDraft()
|
||||
draft.amount = this.self.amount.abs().toString()
|
||||
const user = this.self.user
|
||||
draft.user = new UserIdentifier(user.communityUuid, new IdentifierSeed(this.self.code))
|
||||
draft.linkedUser = new UserIdentifier(user.communityUuid, new CommunityUser(user.gradidoID, 1))
|
||||
draft.createdAt = this.self.deletedAt.toISOString()
|
||||
draft.type = TransactionType.GRADIDO_REDEEM_DEFERRED_TRANSFER
|
||||
return draft
|
||||
}
|
||||
|
||||
protected setJoinIdAndType(dltTransaction: DltTransaction): void {
|
||||
if (!this.self) {
|
||||
throw new LogError('try to create dlt entry for empty transaction link')
|
||||
}
|
||||
dltTransaction.transactionLinkId = this.self.id
|
||||
dltTransaction.typeId = DltTransactionType.DELETE_DEFERRED_TRANSFER
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,59 @@
|
||||
import { DltTransaction } from '@entity/DltTransaction'
|
||||
import { TransactionLink } from '@entity/TransactionLink'
|
||||
|
||||
import { DltTransactionType } from '@dltConnector/enum/DltTransactionType'
|
||||
import { TransactionType } from '@dltConnector/enum/TransactionType'
|
||||
import { CommunityUser } from '@dltConnector/model/CommunityUser'
|
||||
import { IdentifierSeed } from '@dltConnector/model/IdentifierSeed'
|
||||
import { TransactionDraft } from '@dltConnector/model/TransactionDraft'
|
||||
import { UserIdentifier } from '@dltConnector/model/UserIdentifier'
|
||||
|
||||
import { LogError } from '@/server/LogError'
|
||||
|
||||
import { AbstractTransactionToDltRole } from './AbstractTransactionToDlt.role'
|
||||
|
||||
/**
|
||||
* send transactionLink as Deferred Transfers
|
||||
*/
|
||||
export class TransactionLinkToDltRole extends AbstractTransactionToDltRole<TransactionLink> {
|
||||
async initWithLast(): Promise<this> {
|
||||
this.self = await this.createQueryForPendingItems(
|
||||
TransactionLink.createQueryBuilder().leftJoinAndSelect('TransactionLink.user', 'user'),
|
||||
'TransactionLink.id = dltTransaction.transactionLinkId',
|
||||
// eslint-disable-next-line camelcase
|
||||
{ TransactionLink_createdAt: 'ASC', User_id: 'ASC' },
|
||||
).getOne()
|
||||
return this
|
||||
}
|
||||
|
||||
public getTimestamp(): number {
|
||||
if (!this.self) {
|
||||
return Infinity
|
||||
}
|
||||
return this.self.createdAt.getTime()
|
||||
}
|
||||
|
||||
public convertToGraphqlInput(): TransactionDraft {
|
||||
if (!this.self) {
|
||||
throw new LogError('try to create dlt entry for empty transaction link')
|
||||
}
|
||||
const draft = new TransactionDraft()
|
||||
draft.amount = this.self.amount.abs().toString()
|
||||
const user = this.self.user
|
||||
draft.user = new UserIdentifier(user.communityUuid, new CommunityUser(user.gradidoID, 1))
|
||||
draft.linkedUser = new UserIdentifier(user.communityUuid, new IdentifierSeed(this.self.code))
|
||||
draft.createdAt = this.self.createdAt.toISOString()
|
||||
draft.timeoutDuration = (this.self.validUntil.getTime() - this.self.createdAt.getTime()) / 1000
|
||||
draft.memo = this.self.memo
|
||||
draft.type = TransactionType.GRADIDO_DEFERRED_TRANSFER
|
||||
return draft
|
||||
}
|
||||
|
||||
protected setJoinIdAndType(dltTransaction: DltTransaction): void {
|
||||
if (!this.self) {
|
||||
throw new LogError('try to create dlt entry for empty transaction link')
|
||||
}
|
||||
dltTransaction.transactionLinkId = this.self.id
|
||||
dltTransaction.typeId = DltTransactionType.DEFERRED_TRANSFER
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,106 @@
|
||||
import { DltTransaction } from '@entity/DltTransaction'
|
||||
import { Transaction } from '@entity/Transaction'
|
||||
|
||||
import { DltTransactionType } from '@dltConnector/enum/DltTransactionType'
|
||||
import { TransactionType } from '@dltConnector/enum/TransactionType'
|
||||
import { CommunityUser } from '@dltConnector/model/CommunityUser'
|
||||
import { IdentifierSeed } from '@dltConnector/model/IdentifierSeed'
|
||||
import { TransactionDraft } from '@dltConnector/model/TransactionDraft'
|
||||
import { UserIdentifier } from '@dltConnector/model/UserIdentifier'
|
||||
|
||||
import { TransactionTypeId } from '@/graphql/enum/TransactionTypeId'
|
||||
import { LogError } from '@/server/LogError'
|
||||
|
||||
import { AbstractTransactionToDltRole } from './AbstractTransactionToDlt.role'
|
||||
|
||||
/**
|
||||
* send transfer and creations transactions to dlt connector as GradidoTransfer and GradidoCreation
|
||||
*/
|
||||
export class TransactionToDltRole extends AbstractTransactionToDltRole<Transaction> {
|
||||
private type: DltTransactionType
|
||||
async initWithLast(): Promise<this> {
|
||||
this.self = await this.createQueryForPendingItems(
|
||||
Transaction.createQueryBuilder().leftJoinAndSelect(
|
||||
'Transaction.transactionLink',
|
||||
'transactionLink',
|
||||
),
|
||||
'Transaction.id = dltTransaction.transactionId',
|
||||
// eslint-disable-next-line camelcase
|
||||
{ balance_date: 'ASC', Transaction_id: 'ASC' },
|
||||
)
|
||||
// we don't need the receive transactions, there contain basically the same data as the send transactions
|
||||
.andWhere('Transaction.type_id <> :typeId', { typeId: TransactionTypeId.RECEIVE })
|
||||
.getOne()
|
||||
return this
|
||||
}
|
||||
|
||||
public getTimestamp(): number {
|
||||
if (!this.self) {
|
||||
return Infinity
|
||||
}
|
||||
return this.self.balanceDate.getTime()
|
||||
}
|
||||
|
||||
public convertToGraphqlInput(): TransactionDraft {
|
||||
if (!this.self) {
|
||||
throw new LogError('try to create dlt entry for empty transaction')
|
||||
}
|
||||
const draft = new TransactionDraft()
|
||||
draft.amount = this.self.amount.abs().toString()
|
||||
|
||||
switch (this.self.typeId as TransactionTypeId) {
|
||||
case TransactionTypeId.CREATION:
|
||||
draft.type = TransactionType.GRADIDO_CREATION
|
||||
this.type = DltTransactionType.CREATION
|
||||
break
|
||||
case TransactionTypeId.SEND:
|
||||
case TransactionTypeId.RECEIVE:
|
||||
draft.type = TransactionType.GRADIDO_TRANSFER
|
||||
this.type = DltTransactionType.TRANSFER
|
||||
break
|
||||
default:
|
||||
this.type = DltTransactionType.UNKNOWN
|
||||
throw new LogError('wrong role for type', this.self.typeId as TransactionTypeId)
|
||||
}
|
||||
if (
|
||||
!this.self.linkedUserGradidoID ||
|
||||
!this.self.linkedUserCommunityUuid ||
|
||||
!this.self.userCommunityUuid
|
||||
) {
|
||||
throw new LogError(
|
||||
`missing necessary field in transaction: ${this.self.id}, need linkedUserGradidoID, linkedUserCommunityUuid and userCommunityUuid`,
|
||||
)
|
||||
}
|
||||
// it is a redeem of a transaction link?
|
||||
const transactionLink = this.self.transactionLink
|
||||
if (transactionLink) {
|
||||
draft.user = new UserIdentifier(
|
||||
this.self.userCommunityUuid,
|
||||
new IdentifierSeed(transactionLink.code),
|
||||
)
|
||||
draft.type = TransactionType.GRADIDO_REDEEM_DEFERRED_TRANSFER
|
||||
this.type = DltTransactionType.REDEEM_DEFERRED_TRANSFER
|
||||
} else {
|
||||
draft.user = new UserIdentifier(
|
||||
this.self.userCommunityUuid,
|
||||
new CommunityUser(this.self.userGradidoID, 1),
|
||||
)
|
||||
}
|
||||
draft.linkedUser = new UserIdentifier(
|
||||
this.self.linkedUserCommunityUuid,
|
||||
new CommunityUser(this.self.linkedUserGradidoID, 1),
|
||||
)
|
||||
draft.memo = this.self.memo
|
||||
draft.createdAt = this.self.balanceDate.toISOString()
|
||||
draft.targetDate = this.self.creationDate?.toISOString()
|
||||
return draft
|
||||
}
|
||||
|
||||
protected setJoinIdAndType(dltTransaction: DltTransaction): void {
|
||||
if (!this.self) {
|
||||
throw new LogError('try to create dlt entry for empty transaction')
|
||||
}
|
||||
dltTransaction.transactionId = this.self.id
|
||||
dltTransaction.typeId = this.type
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,55 @@
|
||||
import { DltTransaction } from '@entity/DltTransaction'
|
||||
import { User } from '@entity/User'
|
||||
|
||||
import { AccountType } from '@dltConnector/enum/AccountType'
|
||||
import { DltTransactionType } from '@dltConnector/enum/DltTransactionType'
|
||||
import { TransactionType } from '@dltConnector/enum/TransactionType'
|
||||
import { CommunityUser } from '@dltConnector/model/CommunityUser'
|
||||
import { TransactionDraft } from '@dltConnector/model/TransactionDraft'
|
||||
import { UserIdentifier } from '@dltConnector/model/UserIdentifier'
|
||||
|
||||
import { LogError } from '@/server/LogError'
|
||||
|
||||
import { AbstractTransactionToDltRole } from './AbstractTransactionToDlt.role'
|
||||
|
||||
/**
|
||||
* send new user to dlt connector, will be made to RegisterAddress Transaction
|
||||
*/
|
||||
export class UserToDltRole extends AbstractTransactionToDltRole<User> {
|
||||
async initWithLast(): Promise<this> {
|
||||
this.self = await this.createQueryForPendingItems(
|
||||
User.createQueryBuilder(),
|
||||
'User.id = dltTransaction.userId',
|
||||
// eslint-disable-next-line camelcase
|
||||
{ User_created_at: 'ASC', User_id: 'ASC' },
|
||||
).getOne()
|
||||
return this
|
||||
}
|
||||
|
||||
public getTimestamp(): number {
|
||||
if (!this.self) {
|
||||
return Infinity
|
||||
}
|
||||
return this.self.createdAt.getTime()
|
||||
}
|
||||
|
||||
public convertToGraphqlInput(): TransactionDraft {
|
||||
if (!this.self) {
|
||||
throw new LogError('try to create dlt entry for empty transaction')
|
||||
}
|
||||
const draft = new TransactionDraft()
|
||||
draft.user = new UserIdentifier(this.self.communityUuid, new CommunityUser(this.self.gradidoID))
|
||||
draft.createdAt = this.self.createdAt.toISOString()
|
||||
draft.accountType = AccountType.COMMUNITY_HUMAN
|
||||
draft.type = TransactionType.REGISTER_ADDRESS
|
||||
return draft
|
||||
}
|
||||
|
||||
protected setJoinIdAndType(dltTransaction: DltTransaction): void {
|
||||
if (!this.self) {
|
||||
throw new LogError('try to create dlt entry for empty user')
|
||||
}
|
||||
dltTransaction.userId = this.self.id
|
||||
dltTransaction.typeId = DltTransactionType.REGISTER_ADDRESS
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,66 @@
|
||||
import { Transaction } from '@entity/Transaction'
|
||||
import { TransactionLink } from '@entity/TransactionLink'
|
||||
import { User } from '@entity/User'
|
||||
|
||||
import { DltConnectorClient } from '@/apis/dltConnector/DltConnectorClient'
|
||||
import { TransactionResult } from '@/apis/dltConnector/model/TransactionResult'
|
||||
import { backendLogger as logger } from '@/server/logger'
|
||||
|
||||
import { AbstractTransactionToDltRole } from './AbstractTransactionToDlt.role'
|
||||
import { TransactionLinkDeleteToDltRole } from './TransactionLinkDeleteToDlt.role'
|
||||
import { TransactionLinkToDltRole } from './TransactionLinkToDlt.role'
|
||||
import { TransactionToDltRole } from './TransactionToDlt.role'
|
||||
import { UserToDltRole } from './UserToDlt.role'
|
||||
|
||||
/**
|
||||
* @DCI-Context
|
||||
* Context for sending transactions to dlt connector, always the oldest not sended transaction first
|
||||
*/
|
||||
export async function transactionToDlt(dltConnector: DltConnectorClient): Promise<void> {
|
||||
async function findNextPendingTransaction(): Promise<
|
||||
AbstractTransactionToDltRole<Transaction | User | TransactionLink>
|
||||
> {
|
||||
// collect each oldest not sended entity from db and choose oldest
|
||||
const results = await Promise.all([
|
||||
new TransactionToDltRole().initWithLast(),
|
||||
new UserToDltRole().initWithLast(),
|
||||
new TransactionLinkToDltRole().initWithLast(),
|
||||
new TransactionLinkDeleteToDltRole().initWithLast(),
|
||||
])
|
||||
|
||||
// sort array to get oldest at first place
|
||||
results.sort((a, b) => {
|
||||
return a.getTimestamp() - b.getTimestamp()
|
||||
})
|
||||
return results[0]
|
||||
}
|
||||
while (true) {
|
||||
const pendingTransactionRole = await findNextPendingTransaction()
|
||||
const pendingTransaction = pendingTransactionRole.getEntity()
|
||||
if (!pendingTransaction) {
|
||||
break
|
||||
}
|
||||
let messageId = ''
|
||||
let error: string | null = null
|
||||
let result: TransactionResult | undefined
|
||||
try {
|
||||
result = await dltConnector.sendTransaction(pendingTransactionRole.convertToGraphqlInput())
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
error = e.message
|
||||
} else if (typeof e === 'string') {
|
||||
error = e
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
if (result?.succeed && result.recipe) {
|
||||
messageId = result.recipe.messageIdHex
|
||||
} else if (result?.error) {
|
||||
error = result.error.message
|
||||
logger.error('error from dlt-connector', result.error)
|
||||
}
|
||||
|
||||
await pendingTransactionRole.saveTransactionResult(messageId, error)
|
||||
}
|
||||
}
|
||||
10
backend/src/apis/dltConnector/model/CommunityUser.ts
Normal file
10
backend/src/apis/dltConnector/model/CommunityUser.ts
Normal file
@ -0,0 +1,10 @@
|
||||
export class CommunityUser {
|
||||
// for community user, uuid and communityUuid used
|
||||
uuid: string
|
||||
accountNr?: number
|
||||
|
||||
constructor(uuid: string, accountNr?: number) {
|
||||
this.uuid = uuid
|
||||
this.accountNr = accountNr
|
||||
}
|
||||
}
|
||||
9
backend/src/apis/dltConnector/model/IdentifierSeed.ts
Normal file
9
backend/src/apis/dltConnector/model/IdentifierSeed.ts
Normal file
@ -0,0 +1,9 @@
|
||||
// https://www.npmjs.com/package/@apollo/protobufjs
|
||||
|
||||
export class IdentifierSeed {
|
||||
seed: string
|
||||
|
||||
constructor(seed: string) {
|
||||
this.seed = seed
|
||||
}
|
||||
}
|
||||
22
backend/src/apis/dltConnector/model/TransactionDraft.ts
Executable file
22
backend/src/apis/dltConnector/model/TransactionDraft.ts
Executable file
@ -0,0 +1,22 @@
|
||||
// https://www.npmjs.com/package/@apollo/protobufjs
|
||||
import { AccountType } from '@dltConnector/enum/AccountType'
|
||||
import { TransactionType } from '@dltConnector/enum/TransactionType'
|
||||
|
||||
import { UserIdentifier } from './UserIdentifier'
|
||||
|
||||
export class TransactionDraft {
|
||||
user: UserIdentifier
|
||||
// not used for simply register address
|
||||
linkedUser?: UserIdentifier
|
||||
// not used for register address
|
||||
amount?: string
|
||||
memo?: string
|
||||
type: TransactionType
|
||||
createdAt: string
|
||||
// only for creation transaction
|
||||
targetDate?: string
|
||||
// only for deferred transaction
|
||||
timeoutDuration?: number
|
||||
// only for register address
|
||||
accountType?: AccountType
|
||||
}
|
||||
@ -1,8 +1,7 @@
|
||||
import { TransactionType } from '@dltConnector/enum/TransactionType'
|
||||
|
||||
export interface TransactionRecipe {
|
||||
id: number
|
||||
createdAt: string
|
||||
type: TransactionType
|
||||
topic: string
|
||||
messageIdHex: string
|
||||
}
|
||||
|
||||
@ -1,5 +1,17 @@
|
||||
export interface UserIdentifier {
|
||||
uuid: string
|
||||
import { CommunityUser } from './CommunityUser'
|
||||
import { IdentifierSeed } from './IdentifierSeed'
|
||||
|
||||
export class UserIdentifier {
|
||||
communityUuid: string
|
||||
accountNr?: number
|
||||
communityUser?: CommunityUser
|
||||
seed?: IdentifierSeed // used for deferred transfers
|
||||
|
||||
constructor(communityUuid: string, input: CommunityUser | IdentifierSeed) {
|
||||
if (input instanceof CommunityUser) {
|
||||
this.communityUser = input
|
||||
} else if (input instanceof IdentifierSeed) {
|
||||
this.seed = input
|
||||
}
|
||||
this.communityUuid = communityUuid
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,66 @@
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import { CONFIG } from '@/config'
|
||||
import { backendLogger as logger } from '@/server/logger'
|
||||
import { TypeORMError } from '@dbTools/typeorm'
|
||||
// eslint-disable-next-line import/named, n/no-extraneous-import
|
||||
import { FetchError } from 'node-fetch'
|
||||
|
||||
import { DltConnectorClient } from '@dltConnector/DltConnectorClient'
|
||||
|
||||
import { LogError } from '@/server/LogError'
|
||||
import {
|
||||
InterruptiveSleepManager,
|
||||
TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY,
|
||||
} from '@/util/InterruptiveSleepManager'
|
||||
|
||||
import { transactionToDlt } from './interaction/transactionToDlt/transactionToDlt.context'
|
||||
|
||||
let isLoopRunning = true
|
||||
|
||||
export const stopSendTransactionsToDltConnector = (): void => {
|
||||
isLoopRunning = false
|
||||
}
|
||||
|
||||
export async function sendTransactionsToDltConnector(): Promise<void> {
|
||||
const dltConnector = DltConnectorClient.getInstance()
|
||||
|
||||
if (!dltConnector) {
|
||||
logger.info('Sending to DltConnector currently not configured...')
|
||||
isLoopRunning = false
|
||||
return
|
||||
}
|
||||
logger.info('Starting sendTransactionsToDltConnector task')
|
||||
|
||||
// define outside of loop for reuse and reducing gb collection
|
||||
// const queries = getFindNextPendingTransactionQueries()
|
||||
|
||||
// eslint-disable-next-line no-unmodified-loop-condition
|
||||
while (isLoopRunning) {
|
||||
try {
|
||||
// return after no pending transactions are left
|
||||
await transactionToDlt(dltConnector)
|
||||
await InterruptiveSleepManager.getInstance().sleep(
|
||||
TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY,
|
||||
// TODO: put sleep time into config, because it influence performance,
|
||||
// transactionToDlt call 4 db queries to look for new transactions
|
||||
CONFIG.PRODUCTION ? 100000 : 1000,
|
||||
)
|
||||
} catch (e) {
|
||||
// couldn't connect to dlt-connector? We wait
|
||||
if (e instanceof FetchError) {
|
||||
logger.error(`error connecting dlt-connector, wait 5 seconds before retry: ${String(e)}`)
|
||||
await InterruptiveSleepManager.getInstance().sleep(
|
||||
TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY,
|
||||
5000,
|
||||
)
|
||||
} else {
|
||||
if (e instanceof TypeORMError) {
|
||||
// seems to be a error in code, so let better stop here
|
||||
throw new LogError(e.message, e.stack)
|
||||
} else {
|
||||
logger.error(`Error while sending to DLT-connector or writing messageId`, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -59,7 +59,7 @@ import { getOpenCreations, getUserCreation, validateContribution } from './util/
|
||||
import { extractGraphQLFields } from './util/extractGraphQLFields'
|
||||
import { findContributions } from './util/findContributions'
|
||||
import { getLastTransaction } from './util/getLastTransaction'
|
||||
import { sendTransactionsToDltConnector } from './util/sendTransactionsToDltConnector'
|
||||
import { InterruptiveSleepManager, TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY } from '@/util/InterruptiveSleepManager'
|
||||
|
||||
const db = AppDatabase.getInstance()
|
||||
const createLogger = () => getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.resolver.ContributionResolver`)
|
||||
@ -519,8 +519,8 @@ export class ContributionResolver {
|
||||
|
||||
await queryRunner.commitTransaction()
|
||||
|
||||
// trigger to send transaction via dlt-connector
|
||||
await sendTransactionsToDltConnector()
|
||||
// notify dlt-connector loop for new work
|
||||
InterruptiveSleepManager.getInstance().interrupt(TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY)
|
||||
|
||||
logger.info('creation commited successfuly.')
|
||||
await sendContributionConfirmedEmail({
|
||||
|
||||
@ -37,6 +37,10 @@ import {
|
||||
} from '@/event/Events'
|
||||
import { LogError } from '@/server/LogError'
|
||||
import { Context, getClientTimezoneOffset, getUser } from '@/server/context'
|
||||
import {
|
||||
InterruptiveSleepManager,
|
||||
TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY,
|
||||
} from '@/util/InterruptiveSleepManager'
|
||||
import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK'
|
||||
import { TRANSACTION_LINK_LOCK } from '@/util/TRANSACTION_LINK_LOCK'
|
||||
import { calculateDecay } from 'shared'
|
||||
@ -53,7 +57,6 @@ import {
|
||||
} from './util/communities'
|
||||
import { getUserCreation, validateContribution } from './util/creations'
|
||||
import { getLastTransaction } from './util/getLastTransaction'
|
||||
import { sendTransactionsToDltConnector } from './util/sendTransactionsToDltConnector'
|
||||
import { transactionLinkList } from './util/transactionLinkList'
|
||||
|
||||
const createLogger = () => getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.resolver.TransactionLinkResolver`)
|
||||
@ -347,8 +350,9 @@ export class TransactionLinkResolver {
|
||||
} finally {
|
||||
releaseLock()
|
||||
}
|
||||
// trigger to send transaction via dlt-connector
|
||||
await sendTransactionsToDltConnector()
|
||||
// notify dlt-connector loop for new work
|
||||
InterruptiveSleepManager.getInstance().interrupt(TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY)
|
||||
|
||||
return true
|
||||
} else {
|
||||
const now = new Date()
|
||||
@ -398,6 +402,8 @@ export class TransactionLinkResolver {
|
||||
} finally {
|
||||
releaseLinkLock()
|
||||
}
|
||||
// notify dlt-connector loop for new work
|
||||
InterruptiveSleepManager.getInstance().interrupt(TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@ -31,6 +31,10 @@ import { EVENT_TRANSACTION_RECEIVE, EVENT_TRANSACTION_SEND } from '@/event/Event
|
||||
import { SendCoinsResult } from '@/federation/client/1_0/model/SendCoinsResult'
|
||||
import { LogError } from '@/server/LogError'
|
||||
import { Context, getUser } from '@/server/context'
|
||||
import {
|
||||
InterruptiveSleepManager,
|
||||
TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY,
|
||||
} from '@/util/InterruptiveSleepManager'
|
||||
import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK'
|
||||
import { communityUser } from '@/util/communityUser'
|
||||
import { fullName } from '@/util/utilities'
|
||||
@ -48,7 +52,6 @@ import {
|
||||
processXComCommittingSendCoins,
|
||||
processXComPendingSendCoins,
|
||||
} from './util/processXComSendCoins'
|
||||
import { sendTransactionsToDltConnector } from './util/sendTransactionsToDltConnector'
|
||||
import { storeForeignUser } from './util/storeForeignUser'
|
||||
import { transactionLinkSummary } from './util/transactionLinkSummary'
|
||||
|
||||
@ -170,15 +173,14 @@ export const executeTransaction = async (
|
||||
transactionReceive,
|
||||
transactionReceive.amount,
|
||||
)
|
||||
|
||||
// trigger to send transaction via dlt-connector
|
||||
await sendTransactionsToDltConnector()
|
||||
} catch (e) {
|
||||
await queryRunner.rollbackTransaction()
|
||||
throw new LogError('Transaction was not successful', e)
|
||||
} finally {
|
||||
await queryRunner.release()
|
||||
}
|
||||
// notify dlt-connector loop for new work
|
||||
InterruptiveSleepManager.getInstance().interrupt(TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY)
|
||||
await sendTransactionReceivedEmail({
|
||||
firstName: recipient.firstName,
|
||||
lastName: recipient.lastName,
|
||||
|
||||
@ -85,6 +85,10 @@ import { Context, getClientTimezoneOffset, getUser } from '@/server/context'
|
||||
import { communityDbUser } from '@/util/communityUser'
|
||||
import { hasElopageBuys } from '@/util/hasElopageBuys'
|
||||
import { durationInMinutesFromDates, getTimeDurationObject, printTimeDuration } from '@/util/time'
|
||||
import {
|
||||
InterruptiveSleepManager,
|
||||
TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY,
|
||||
} from '@/util/InterruptiveSleepManager'
|
||||
import { delay } from '@/util/utilities'
|
||||
|
||||
import random from 'random-bigint'
|
||||
@ -479,6 +483,9 @@ export class UserResolver {
|
||||
}
|
||||
}
|
||||
|
||||
// notify dlt-connector loop for new work
|
||||
InterruptiveSleepManager.getInstance().interrupt(TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY)
|
||||
|
||||
if (redeemCode) {
|
||||
eventRegisterRedeem.affectedUser = dbUser
|
||||
eventRegisterRedeem.actingUser = dbUser
|
||||
|
||||
@ -1,85 +0,0 @@
|
||||
import { DltTransaction, Transaction } from 'database'
|
||||
import { IsNull } from 'typeorm'
|
||||
|
||||
import { DltConnectorClient } from '@dltConnector/DltConnectorClient'
|
||||
|
||||
import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const'
|
||||
import { Monitor, MonitorNames } from '@/util/Monitor'
|
||||
import { getLogger } from 'log4js'
|
||||
|
||||
const logger = getLogger(
|
||||
`${LOG4JS_BASE_CATEGORY_NAME}.graphql.resolver.util.sendTransactionsToDltConnector`,
|
||||
)
|
||||
|
||||
export async function sendTransactionsToDltConnector(): Promise<void> {
|
||||
logger.info('sendTransactionsToDltConnector...')
|
||||
// check if this logic is still occupied, no concurrecy allowed
|
||||
if (!Monitor.isLocked(MonitorNames.SEND_DLT_TRANSACTIONS)) {
|
||||
// mark this block for occuption to prevent concurrency
|
||||
Monitor.lockIt(MonitorNames.SEND_DLT_TRANSACTIONS)
|
||||
|
||||
try {
|
||||
await createDltTransactions()
|
||||
const dltConnector = DltConnectorClient.getInstance()
|
||||
if (dltConnector) {
|
||||
logger.debug('with sending to DltConnector...')
|
||||
const dltTransactions = await DltTransaction.find({
|
||||
where: { messageId: IsNull() },
|
||||
relations: ['transaction'],
|
||||
order: { createdAt: 'ASC', id: 'ASC' },
|
||||
})
|
||||
|
||||
for (const dltTx of dltTransactions) {
|
||||
if (!dltTx.transaction) {
|
||||
continue
|
||||
}
|
||||
try {
|
||||
const result = await dltConnector.transmitTransaction(dltTx.transaction)
|
||||
// message id isn't known at this point of time, because transaction will not direct sended to iota,
|
||||
// it will first go to db and then sended, if no transaction is in db before
|
||||
if (result) {
|
||||
dltTx.messageId = 'sended'
|
||||
await DltTransaction.save(dltTx)
|
||||
logger.info(`store messageId=${dltTx.messageId} in dltTx=${dltTx.id}`)
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
`error while sending to dlt-connector or writing messageId of dltTx=${dltTx.id}`,
|
||||
e,
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logger.info('sending to DltConnector currently not configured...')
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error('error on sending transactions to dlt-connector.', e)
|
||||
} finally {
|
||||
// releae Monitor occupation
|
||||
Monitor.releaseIt(MonitorNames.SEND_DLT_TRANSACTIONS)
|
||||
}
|
||||
} else {
|
||||
logger.info('sendTransactionsToDltConnector currently locked by monitor...')
|
||||
}
|
||||
}
|
||||
|
||||
async function createDltTransactions(): Promise<void> {
|
||||
const dltqb = DltTransaction.createQueryBuilder().select('transactions_id')
|
||||
const newTransactions: Transaction[] = await Transaction.createQueryBuilder()
|
||||
.select('id')
|
||||
.addSelect('balance_date')
|
||||
.where('id NOT IN (' + dltqb.getSql() + ')')
|
||||
|
||||
.orderBy({ balance_date: 'ASC', id: 'ASC' })
|
||||
.getRawMany()
|
||||
|
||||
const dltTxArray: DltTransaction[] = []
|
||||
let idx = 0
|
||||
while (newTransactions.length > dltTxArray.length) {
|
||||
// timing problems with for(let idx = 0; idx < newTransactions.length; idx++) {
|
||||
const dltTx = DltTransaction.create()
|
||||
dltTx.transactionId = newTransactions[idx++].id
|
||||
await DltTransaction.save(dltTx)
|
||||
dltTxArray.push(dltTx)
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
import 'reflect-metadata'
|
||||
import 'source-map-support/register'
|
||||
import { getLogger } from 'log4js'
|
||||
import { sendTransactionsToDltConnector } from './apis/dltConnector/sendTransactionsToDltConnector'
|
||||
import { CONFIG } from './config'
|
||||
import { startValidateCommunities } from './federation/validateCommunities'
|
||||
import { createServer } from './server/createServer'
|
||||
@ -18,7 +19,11 @@ async function main() {
|
||||
console.log(`GraphIQL available at http://localhost:${CONFIG.PORT}`)
|
||||
}
|
||||
})
|
||||
await startValidateCommunities(Number(CONFIG.FEDERATION_VALIDATE_COMMUNITY_TIMER))
|
||||
// task is running the whole time for transmitting transaction via dlt-connector to iota
|
||||
// can be notified with InterruptiveSleepManager.getInstance().interrupt(TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY)
|
||||
// that a new transaction or user was stored in db
|
||||
void sendTransactionsToDltConnector()
|
||||
void startValidateCommunities(Number(CONFIG.FEDERATION_VALIDATE_COMMUNITY_TIMER))
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import { delay } from './utilities'
|
||||
|
||||
/**
|
||||
* Sleep, that can be interrupted
|
||||
* call sleep only for msSteps and than check if interrupt was called
|
||||
@ -14,17 +16,11 @@ export class InterruptiveSleep {
|
||||
this.interruptSleep = true
|
||||
}
|
||||
|
||||
private static _sleep(ms: number) {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, ms)
|
||||
})
|
||||
}
|
||||
|
||||
public async sleep(ms: number): Promise<void> {
|
||||
let waited = 0
|
||||
this.interruptSleep = false
|
||||
while (waited < ms && !this.interruptSleep) {
|
||||
await InterruptiveSleep._sleep(this.msSteps)
|
||||
await delay(this.msSteps)
|
||||
waited += this.msSteps
|
||||
}
|
||||
}
|
||||
@ -1,13 +1,17 @@
|
||||
import { LogError } from '@/server/LogError'
|
||||
|
||||
import { InterruptiveSleep } from '../utils/InterruptiveSleep'
|
||||
import { InterruptiveSleep } from './InterruptiveSleep'
|
||||
|
||||
// Source: https://refactoring.guru/design-patterns/singleton/typescript/example
|
||||
// and ../federation/client/FederationClientFactory.ts
|
||||
/**
|
||||
* A Singleton class defines the `getInstance` method that lets clients access
|
||||
* the unique singleton instance.
|
||||
* Managing Instances of interruptive sleep it is inspired from conditions from c++ multithreading
|
||||
* It is used for separate worker threads which will go to sleep after they haven't anything todo left,
|
||||
* but with this Manager and InterruptiveSleep Object it sleeps only stepSize and check if something interrupted his sleep,
|
||||
* so he can check for new work
|
||||
*/
|
||||
export const TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY = 'transmitToIota'
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-extraneous-class
|
||||
export class InterruptiveSleepManager {
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
7874
backend/yarn.lock
7874
backend/yarn.lock
File diff suppressed because it is too large
Load Diff
36
database/entity/0088-add_dlt_users_table/DltTransaction.ts
Normal file
36
database/entity/0088-add_dlt_users_table/DltTransaction.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import { BaseEntity, Entity, PrimaryGeneratedColumn, Column, OneToOne, JoinColumn } from 'typeorm'
|
||||
import { Transaction } from '../Transaction'
|
||||
|
||||
@Entity('dlt_transactions', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' })
|
||||
export class DltTransaction extends BaseEntity {
|
||||
@PrimaryGeneratedColumn('increment', { unsigned: true })
|
||||
id: number
|
||||
|
||||
@Column({ name: 'transaction_id', type: 'int', unsigned: true, nullable: false })
|
||||
transactionId: number
|
||||
|
||||
@Column({
|
||||
name: 'message_id',
|
||||
length: 64,
|
||||
nullable: true,
|
||||
default: null,
|
||||
collation: 'utf8mb4_unicode_ci',
|
||||
})
|
||||
messageId: string
|
||||
|
||||
@Column({ name: 'verified', type: 'bool', nullable: false, default: false })
|
||||
verified: boolean
|
||||
|
||||
@Column({ name: 'created_at', default: () => 'CURRENT_TIMESTAMP(3)', nullable: false })
|
||||
createdAt: Date
|
||||
|
||||
@Column({ name: 'verified_at', nullable: true, default: null, type: 'datetime' })
|
||||
verifiedAt: Date | null
|
||||
|
||||
@Column({ name: 'error', type: 'text', nullable: true })
|
||||
error: string | null
|
||||
|
||||
@OneToOne(() => Transaction, (transaction) => transaction.dltTransaction)
|
||||
@JoinColumn({ name: 'transaction_id' })
|
||||
transaction?: Transaction | null
|
||||
}
|
||||
37
database/entity/0088-add_dlt_users_table/DltUser.ts
Normal file
37
database/entity/0088-add_dlt_users_table/DltUser.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { BaseEntity, Entity, PrimaryGeneratedColumn, Column, OneToOne, JoinColumn } from 'typeorm'
|
||||
// this Entity was removed in current code and isn't any longer compatible with user
|
||||
import { User } from './User'
|
||||
|
||||
@Entity('dlt_users', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' })
|
||||
export class DltUser extends BaseEntity {
|
||||
@PrimaryGeneratedColumn('increment', { unsigned: true })
|
||||
id: number
|
||||
|
||||
@Column({ name: 'user_id', type: 'int', unsigned: true, nullable: false })
|
||||
userId: number
|
||||
|
||||
@Column({
|
||||
name: 'message_id',
|
||||
length: 64,
|
||||
nullable: true,
|
||||
default: null,
|
||||
collation: 'utf8mb4_unicode_ci',
|
||||
})
|
||||
messageId: string
|
||||
|
||||
@Column({ name: 'verified', type: 'bool', nullable: false, default: false })
|
||||
verified: boolean
|
||||
|
||||
@Column({ name: 'created_at', default: () => 'CURRENT_TIMESTAMP(3)', nullable: false })
|
||||
createdAt: Date
|
||||
|
||||
@Column({ name: 'verified_at', nullable: true, default: null, type: 'datetime' })
|
||||
verifiedAt: Date | null
|
||||
|
||||
@Column({ name: 'error', type: 'text', nullable: true })
|
||||
error: string | null
|
||||
|
||||
@OneToOne(() => User, (user) => user.dltUser)
|
||||
@JoinColumn({ name: 'user_id' })
|
||||
user?: User | null
|
||||
}
|
||||
182
database/entity/0088-add_dlt_users_table/User.ts
Normal file
182
database/entity/0088-add_dlt_users_table/User.ts
Normal file
@ -0,0 +1,182 @@
|
||||
import {
|
||||
BaseEntity,
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
DeleteDateColumn,
|
||||
OneToMany,
|
||||
JoinColumn,
|
||||
OneToOne,
|
||||
Geometry,
|
||||
ManyToOne,
|
||||
} from 'typeorm'
|
||||
import { Contribution } from '../Contribution'
|
||||
import { ContributionMessage } from '../ContributionMessage'
|
||||
import { UserContact } from '../UserContact'
|
||||
import { UserRole } from '../UserRole'
|
||||
import { GeometryTransformer } from '../../src/typeorm/GeometryTransformer'
|
||||
import { Community } from '../Community'
|
||||
// removed in current version
|
||||
import { DltUser } from './DltUser'
|
||||
|
||||
@Entity('users', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' })
|
||||
export class User extends BaseEntity {
|
||||
@PrimaryGeneratedColumn('increment', { unsigned: true })
|
||||
id: number
|
||||
|
||||
@Column({ type: 'bool', default: false })
|
||||
foreign: boolean
|
||||
|
||||
@Column({
|
||||
name: 'gradido_id',
|
||||
length: 36,
|
||||
nullable: false,
|
||||
collation: 'utf8mb4_unicode_ci',
|
||||
})
|
||||
gradidoID: string
|
||||
|
||||
@Column({
|
||||
name: 'community_uuid',
|
||||
type: 'char',
|
||||
length: 36,
|
||||
nullable: true,
|
||||
collation: 'utf8mb4_unicode_ci',
|
||||
})
|
||||
communityUuid: string
|
||||
|
||||
@ManyToOne(() => Community, (community) => community.users)
|
||||
@JoinColumn({ name: 'community_uuid', referencedColumnName: 'communityUuid' })
|
||||
community: Community | null
|
||||
|
||||
@Column({
|
||||
name: 'alias',
|
||||
length: 20,
|
||||
nullable: true,
|
||||
default: null,
|
||||
collation: 'utf8mb4_unicode_ci',
|
||||
})
|
||||
alias: string
|
||||
|
||||
@OneToOne(() => UserContact, (emailContact: UserContact) => emailContact.user)
|
||||
@JoinColumn({ name: 'email_id' })
|
||||
emailContact: UserContact
|
||||
|
||||
@Column({ name: 'email_id', type: 'int', unsigned: true, nullable: true, default: null })
|
||||
emailId: number | null
|
||||
|
||||
@Column({
|
||||
name: 'first_name',
|
||||
length: 255,
|
||||
nullable: true,
|
||||
default: null,
|
||||
collation: 'utf8mb4_unicode_ci',
|
||||
})
|
||||
firstName: string
|
||||
|
||||
@Column({
|
||||
name: 'last_name',
|
||||
length: 255,
|
||||
nullable: true,
|
||||
default: null,
|
||||
collation: 'utf8mb4_unicode_ci',
|
||||
})
|
||||
lastName: string
|
||||
|
||||
@Column({ name: 'gms_publish_name', type: 'int', unsigned: true, nullable: false, default: 0 })
|
||||
gmsPublishName: number
|
||||
|
||||
@Column({ name: 'humhub_publish_name', type: 'int', unsigned: true, nullable: false, default: 0 })
|
||||
humhubPublishName: number
|
||||
|
||||
@Column({ name: 'created_at', default: () => 'CURRENT_TIMESTAMP(3)', nullable: false })
|
||||
createdAt: Date
|
||||
|
||||
@DeleteDateColumn({ name: 'deleted_at', nullable: true })
|
||||
deletedAt: Date | null
|
||||
|
||||
@Column({ type: 'bigint', default: 0, unsigned: true })
|
||||
password: BigInt
|
||||
|
||||
@Column({
|
||||
name: 'password_encryption_type',
|
||||
type: 'int',
|
||||
unsigned: true,
|
||||
nullable: false,
|
||||
default: 0,
|
||||
})
|
||||
passwordEncryptionType: number
|
||||
|
||||
@Column({ length: 4, default: 'de', collation: 'utf8mb4_unicode_ci', nullable: false })
|
||||
language: string
|
||||
|
||||
@Column({ type: 'bool', default: false })
|
||||
hideAmountGDD: boolean
|
||||
|
||||
@Column({ type: 'bool', default: false })
|
||||
hideAmountGDT: boolean
|
||||
|
||||
@OneToMany(() => UserRole, (userRole) => userRole.user)
|
||||
@JoinColumn({ name: 'user_id' })
|
||||
userRoles: UserRole[]
|
||||
|
||||
@Column({ name: 'referrer_id', type: 'int', unsigned: true, nullable: true, default: null })
|
||||
referrerId?: number | null
|
||||
|
||||
@Column({
|
||||
name: 'contribution_link_id',
|
||||
type: 'int',
|
||||
unsigned: true,
|
||||
nullable: true,
|
||||
default: null,
|
||||
})
|
||||
contributionLinkId?: number | null
|
||||
|
||||
@Column({ name: 'publisher_id', default: 0 })
|
||||
publisherId: number
|
||||
|
||||
@Column({ name: 'gms_allowed', type: 'bool', default: true })
|
||||
gmsAllowed: boolean
|
||||
|
||||
@Column({
|
||||
name: 'location',
|
||||
type: 'geometry',
|
||||
default: null,
|
||||
nullable: true,
|
||||
transformer: GeometryTransformer,
|
||||
})
|
||||
location: Geometry | null
|
||||
|
||||
@Column({
|
||||
name: 'gms_publish_location',
|
||||
type: 'int',
|
||||
unsigned: true,
|
||||
nullable: false,
|
||||
default: 2,
|
||||
})
|
||||
gmsPublishLocation: number
|
||||
|
||||
@Column({ name: 'gms_registered', type: 'bool', default: false })
|
||||
gmsRegistered: boolean
|
||||
|
||||
@Column({ name: 'gms_registered_at', type: 'datetime', default: null, nullable: true })
|
||||
gmsRegisteredAt: Date | null
|
||||
|
||||
@Column({ name: 'humhub_allowed', type: 'bool', default: false })
|
||||
humhubAllowed: boolean
|
||||
|
||||
@OneToMany(() => Contribution, (contribution) => contribution.user)
|
||||
@JoinColumn({ name: 'user_id' })
|
||||
contributions?: Contribution[]
|
||||
|
||||
@OneToMany(() => ContributionMessage, (message) => message.user)
|
||||
@JoinColumn({ name: 'user_id' })
|
||||
messages?: ContributionMessage[]
|
||||
|
||||
@OneToMany(() => UserContact, (userContact: UserContact) => userContact.user)
|
||||
@JoinColumn({ name: 'user_id' })
|
||||
userContacts?: UserContact[]
|
||||
|
||||
@OneToOne(() => DltUser, (dlt) => dlt.userId)
|
||||
@JoinColumn({ name: 'id', referencedColumnName: 'userId' })
|
||||
dltUser?: DltUser | null
|
||||
}
|
||||
55
database/entity/0089-merge_dlt_tables/DltTransaction.ts
Normal file
55
database/entity/0089-merge_dlt_tables/DltTransaction.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import { BaseEntity, Entity, PrimaryGeneratedColumn, Column, OneToOne, JoinColumn } from 'typeorm'
|
||||
import { Transaction } from '../Transaction'
|
||||
import { User } from '../User'
|
||||
import { TransactionLink } from '../TransactionLink'
|
||||
|
||||
@Entity('dlt_transactions', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' })
|
||||
export class DltTransaction extends BaseEntity {
|
||||
@PrimaryGeneratedColumn('increment', { unsigned: true })
|
||||
id: number
|
||||
|
||||
@Column({ name: 'transaction_id', type: 'int', unsigned: true, nullable: true })
|
||||
transactionId?: number | null
|
||||
|
||||
@Column({ name: 'user_id', type: 'int', unsigned: true, nullable: true })
|
||||
userId?: number | null
|
||||
|
||||
@Column({ name: 'transaction_link_id', type: 'int', unsigned: true, nullable: true })
|
||||
transactionLinkId?: number | null
|
||||
|
||||
@Column({ name: 'type_id', unsigned: true, nullable: false })
|
||||
typeId: number
|
||||
|
||||
@Column({
|
||||
name: 'message_id',
|
||||
length: 64,
|
||||
nullable: true,
|
||||
default: null,
|
||||
collation: 'utf8mb4_unicode_ci',
|
||||
})
|
||||
messageId: string
|
||||
|
||||
@Column({ name: 'verified', type: 'bool', nullable: false, default: false })
|
||||
verified: boolean
|
||||
|
||||
@Column({ name: 'created_at', default: () => 'CURRENT_TIMESTAMP(3)', nullable: false })
|
||||
createdAt: Date
|
||||
|
||||
@Column({ name: 'verified_at', nullable: true, default: null, type: 'datetime' })
|
||||
verifiedAt: Date | null
|
||||
|
||||
@Column({ name: 'error', type: 'text', nullable: true })
|
||||
error: string | null
|
||||
|
||||
@OneToOne(() => Transaction, (transaction) => transaction.dltTransaction)
|
||||
@JoinColumn({ name: 'transaction_id' })
|
||||
transaction?: Transaction | null
|
||||
|
||||
@OneToOne(() => User, (user) => user.dltTransaction)
|
||||
@JoinColumn({ name: 'user_id' })
|
||||
user?: User | null
|
||||
|
||||
@OneToOne(() => TransactionLink, (transactionLink) => transactionLink.dltTransaction)
|
||||
@JoinColumn({ name: 'transaction_link_id' })
|
||||
transactionLink?: TransactionLink | null
|
||||
}
|
||||
176
database/entity/0089-merge_dlt_tables/Transaction.ts
Normal file
176
database/entity/0089-merge_dlt_tables/Transaction.ts
Normal file
@ -0,0 +1,176 @@
|
||||
/* eslint-disable no-use-before-define */
|
||||
import { Decimal } from 'decimal.js-light'
|
||||
import {
|
||||
BaseEntity,
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
OneToOne,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
} from 'typeorm'
|
||||
import { DecimalTransformer } from '../../src/typeorm/DecimalTransformer'
|
||||
import { Contribution } from '../Contribution'
|
||||
import { DltTransaction } from '../DltTransaction'
|
||||
import { TransactionLink } from '../TransactionLink'
|
||||
|
||||
@Entity('transactions')
|
||||
export class Transaction extends BaseEntity {
|
||||
@PrimaryGeneratedColumn('increment', { unsigned: true })
|
||||
id: number
|
||||
|
||||
@Column({ type: 'int', unsigned: true, unique: true, nullable: true, default: null })
|
||||
previous: number | null
|
||||
|
||||
@Column({ name: 'type_id', unsigned: true, nullable: false })
|
||||
typeId: number
|
||||
|
||||
@Column({
|
||||
name: 'transaction_link_id',
|
||||
type: 'int',
|
||||
unsigned: true,
|
||||
nullable: true,
|
||||
default: null,
|
||||
})
|
||||
transactionLinkId?: number | null
|
||||
|
||||
@Column({
|
||||
type: 'decimal',
|
||||
precision: 40,
|
||||
scale: 20,
|
||||
nullable: false,
|
||||
transformer: DecimalTransformer,
|
||||
})
|
||||
amount: Decimal
|
||||
|
||||
@Column({
|
||||
type: 'decimal',
|
||||
precision: 40,
|
||||
scale: 20,
|
||||
nullable: false,
|
||||
transformer: DecimalTransformer,
|
||||
})
|
||||
balance: Decimal
|
||||
|
||||
@Column({
|
||||
name: 'balance_date',
|
||||
type: 'datetime',
|
||||
default: () => 'CURRENT_TIMESTAMP',
|
||||
nullable: false,
|
||||
})
|
||||
balanceDate: Date
|
||||
|
||||
@Column({
|
||||
type: 'decimal',
|
||||
precision: 40,
|
||||
scale: 20,
|
||||
nullable: false,
|
||||
transformer: DecimalTransformer,
|
||||
})
|
||||
decay: Decimal
|
||||
|
||||
@Column({
|
||||
name: 'decay_start',
|
||||
type: 'datetime',
|
||||
nullable: true,
|
||||
default: null,
|
||||
})
|
||||
decayStart: Date | null
|
||||
|
||||
@Column({ length: 255, nullable: false, collation: 'utf8mb4_unicode_ci' })
|
||||
memo: string
|
||||
|
||||
@Column({ name: 'creation_date', type: 'datetime', nullable: true, default: null })
|
||||
creationDate: Date | null
|
||||
|
||||
@Column({ name: 'user_id', unsigned: true, nullable: false })
|
||||
userId: number
|
||||
|
||||
@Column({
|
||||
name: 'user_community_uuid',
|
||||
type: 'varchar',
|
||||
length: 36,
|
||||
nullable: true,
|
||||
collation: 'utf8mb4_unicode_ci',
|
||||
})
|
||||
userCommunityUuid: string | null
|
||||
|
||||
@Column({
|
||||
name: 'user_gradido_id',
|
||||
type: 'varchar',
|
||||
length: 36,
|
||||
nullable: false,
|
||||
collation: 'utf8mb4_unicode_ci',
|
||||
})
|
||||
userGradidoID: string
|
||||
|
||||
@Column({
|
||||
name: 'user_name',
|
||||
type: 'varchar',
|
||||
length: 512,
|
||||
nullable: true,
|
||||
collation: 'utf8mb4_unicode_ci',
|
||||
})
|
||||
userName: string | null
|
||||
|
||||
@Column({
|
||||
name: 'linked_user_id',
|
||||
type: 'int',
|
||||
unsigned: true,
|
||||
nullable: true,
|
||||
default: null,
|
||||
})
|
||||
linkedUserId?: number | null
|
||||
|
||||
@Column({
|
||||
name: 'linked_user_community_uuid',
|
||||
type: 'varchar',
|
||||
length: 36,
|
||||
nullable: true,
|
||||
collation: 'utf8mb4_unicode_ci',
|
||||
})
|
||||
linkedUserCommunityUuid: string | null
|
||||
|
||||
@Column({
|
||||
name: 'linked_user_gradido_id',
|
||||
type: 'varchar',
|
||||
length: 36,
|
||||
nullable: true,
|
||||
collation: 'utf8mb4_unicode_ci',
|
||||
})
|
||||
linkedUserGradidoID: string | null
|
||||
|
||||
@Column({
|
||||
name: 'linked_user_name',
|
||||
type: 'varchar',
|
||||
length: 512,
|
||||
nullable: true,
|
||||
collation: 'utf8mb4_unicode_ci',
|
||||
})
|
||||
linkedUserName: string | null
|
||||
|
||||
@Column({
|
||||
name: 'linked_transaction_id',
|
||||
type: 'int',
|
||||
unsigned: true,
|
||||
nullable: true,
|
||||
default: null,
|
||||
})
|
||||
linkedTransactionId?: number | null
|
||||
|
||||
@OneToOne(() => Contribution, (contribution) => contribution.transaction)
|
||||
@JoinColumn({ name: 'id', referencedColumnName: 'transactionId' })
|
||||
contribution?: Contribution | null
|
||||
|
||||
@OneToOne(() => DltTransaction, (dlt) => dlt.transactionId)
|
||||
@JoinColumn({ name: 'id', referencedColumnName: 'transactionId' })
|
||||
dltTransaction?: DltTransaction | null
|
||||
|
||||
@OneToOne(() => Transaction)
|
||||
@JoinColumn({ name: 'previous' })
|
||||
previousTransaction?: Transaction | null
|
||||
|
||||
@ManyToOne(() => TransactionLink, (transactionLink) => transactionLink.transactions)
|
||||
@JoinColumn({ name: 'transaction_link_id' })
|
||||
transactionLink?: TransactionLink | null
|
||||
}
|
||||
85
database/entity/0089-merge_dlt_tables/TransactionLink.ts
Normal file
85
database/entity/0089-merge_dlt_tables/TransactionLink.ts
Normal file
@ -0,0 +1,85 @@
|
||||
import { Decimal } from 'decimal.js-light'
|
||||
import {
|
||||
BaseEntity,
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
DeleteDateColumn,
|
||||
OneToOne,
|
||||
JoinColumn,
|
||||
OneToMany,
|
||||
} from 'typeorm'
|
||||
import { DecimalTransformer } from '../../src/typeorm/DecimalTransformer'
|
||||
import { DltTransaction } from '../DltTransaction'
|
||||
import { User } from '../User'
|
||||
import { Transaction } from '../Transaction'
|
||||
|
||||
@Entity('transaction_links')
|
||||
export class TransactionLink extends BaseEntity {
|
||||
@PrimaryGeneratedColumn('increment', { unsigned: true })
|
||||
id: number
|
||||
|
||||
@Column({ unsigned: true, nullable: false })
|
||||
userId: number
|
||||
|
||||
@Column({
|
||||
type: 'decimal',
|
||||
precision: 40,
|
||||
scale: 20,
|
||||
nullable: false,
|
||||
transformer: DecimalTransformer,
|
||||
})
|
||||
amount: Decimal
|
||||
|
||||
@Column({
|
||||
type: 'decimal',
|
||||
name: 'hold_available_amount',
|
||||
precision: 40,
|
||||
scale: 20,
|
||||
nullable: false,
|
||||
transformer: DecimalTransformer,
|
||||
})
|
||||
holdAvailableAmount: Decimal
|
||||
|
||||
@Column({ length: 255, nullable: false, collation: 'utf8mb4_unicode_ci' })
|
||||
memo: string
|
||||
|
||||
@Column({ length: 24, nullable: false, collation: 'utf8mb4_unicode_ci' })
|
||||
code: string
|
||||
|
||||
@Column({
|
||||
type: 'datetime',
|
||||
nullable: false,
|
||||
})
|
||||
createdAt: Date
|
||||
|
||||
@DeleteDateColumn()
|
||||
deletedAt: Date | null
|
||||
|
||||
@Column({
|
||||
type: 'datetime',
|
||||
nullable: false,
|
||||
})
|
||||
validUntil: Date
|
||||
|
||||
@Column({
|
||||
type: 'datetime',
|
||||
nullable: true,
|
||||
})
|
||||
redeemedAt: Date | null
|
||||
|
||||
@Column({ type: 'int', unsigned: true, nullable: true })
|
||||
redeemedBy: number | null
|
||||
|
||||
@OneToOne(() => DltTransaction, (dlt) => dlt.transactionLinkId)
|
||||
@JoinColumn({ name: 'id', referencedColumnName: 'transactionLinkId' })
|
||||
dltTransaction?: DltTransaction | null
|
||||
|
||||
@OneToOne(() => User, (user) => user.transactionLink)
|
||||
@JoinColumn({ name: 'userId' })
|
||||
user: User
|
||||
|
||||
@OneToMany(() => Transaction, (transaction) => transaction.transactionLink)
|
||||
@JoinColumn({ referencedColumnName: 'transaction_link_id' })
|
||||
transactions: Transaction[]
|
||||
}
|
||||
186
database/entity/0089-merge_dlt_tables/User.ts
Normal file
186
database/entity/0089-merge_dlt_tables/User.ts
Normal file
@ -0,0 +1,186 @@
|
||||
import {
|
||||
BaseEntity,
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
DeleteDateColumn,
|
||||
OneToMany,
|
||||
JoinColumn,
|
||||
OneToOne,
|
||||
Geometry,
|
||||
ManyToOne,
|
||||
} from 'typeorm'
|
||||
import { Contribution } from '../Contribution'
|
||||
import { ContributionMessage } from '../ContributionMessage'
|
||||
import { UserContact } from '../UserContact'
|
||||
import { UserRole } from '../UserRole'
|
||||
import { GeometryTransformer } from '../../src/typeorm/GeometryTransformer'
|
||||
import { Community } from '../Community'
|
||||
import { DltTransaction } from '../DltTransaction'
|
||||
import { TransactionLink } from './TransactionLink'
|
||||
|
||||
@Entity('users', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' })
|
||||
export class User extends BaseEntity {
|
||||
@PrimaryGeneratedColumn('increment', { unsigned: true })
|
||||
id: number
|
||||
|
||||
@Column({ type: 'bool', default: false })
|
||||
foreign: boolean
|
||||
|
||||
@Column({
|
||||
name: 'gradido_id',
|
||||
length: 36,
|
||||
nullable: false,
|
||||
collation: 'utf8mb4_unicode_ci',
|
||||
})
|
||||
gradidoID: string
|
||||
|
||||
@Column({
|
||||
name: 'community_uuid',
|
||||
type: 'char',
|
||||
length: 36,
|
||||
nullable: true,
|
||||
collation: 'utf8mb4_unicode_ci',
|
||||
})
|
||||
communityUuid: string
|
||||
|
||||
@ManyToOne(() => Community, (community) => community.users)
|
||||
@JoinColumn({ name: 'community_uuid', referencedColumnName: 'communityUuid' })
|
||||
community: Community | null
|
||||
|
||||
@Column({
|
||||
name: 'alias',
|
||||
length: 20,
|
||||
nullable: true,
|
||||
default: null,
|
||||
collation: 'utf8mb4_unicode_ci',
|
||||
})
|
||||
alias: string
|
||||
|
||||
@OneToOne(() => UserContact, (emailContact: UserContact) => emailContact.user)
|
||||
@JoinColumn({ name: 'email_id' })
|
||||
emailContact: UserContact
|
||||
|
||||
@Column({ name: 'email_id', type: 'int', unsigned: true, nullable: true, default: null })
|
||||
emailId: number | null
|
||||
|
||||
@Column({
|
||||
name: 'first_name',
|
||||
length: 255,
|
||||
nullable: true,
|
||||
default: null,
|
||||
collation: 'utf8mb4_unicode_ci',
|
||||
})
|
||||
firstName: string
|
||||
|
||||
@Column({
|
||||
name: 'last_name',
|
||||
length: 255,
|
||||
nullable: true,
|
||||
default: null,
|
||||
collation: 'utf8mb4_unicode_ci',
|
||||
})
|
||||
lastName: string
|
||||
|
||||
@Column({ name: 'gms_publish_name', type: 'int', unsigned: true, nullable: false, default: 0 })
|
||||
gmsPublishName: number
|
||||
|
||||
@Column({ name: 'humhub_publish_name', type: 'int', unsigned: true, nullable: false, default: 0 })
|
||||
humhubPublishName: number
|
||||
|
||||
@Column({ name: 'created_at', default: () => 'CURRENT_TIMESTAMP(3)', nullable: false })
|
||||
createdAt: Date
|
||||
|
||||
@DeleteDateColumn({ name: 'deleted_at', nullable: true })
|
||||
deletedAt: Date | null
|
||||
|
||||
@Column({ type: 'bigint', default: 0, unsigned: true })
|
||||
password: BigInt
|
||||
|
||||
@Column({
|
||||
name: 'password_encryption_type',
|
||||
type: 'int',
|
||||
unsigned: true,
|
||||
nullable: false,
|
||||
default: 0,
|
||||
})
|
||||
passwordEncryptionType: number
|
||||
|
||||
@Column({ length: 4, default: 'de', collation: 'utf8mb4_unicode_ci', nullable: false })
|
||||
language: string
|
||||
|
||||
@Column({ type: 'bool', default: false })
|
||||
hideAmountGDD: boolean
|
||||
|
||||
@Column({ type: 'bool', default: false })
|
||||
hideAmountGDT: boolean
|
||||
|
||||
@OneToMany(() => UserRole, (userRole) => userRole.user)
|
||||
@JoinColumn({ name: 'user_id' })
|
||||
userRoles: UserRole[]
|
||||
|
||||
@Column({ name: 'referrer_id', type: 'int', unsigned: true, nullable: true, default: null })
|
||||
referrerId?: number | null
|
||||
|
||||
@Column({
|
||||
name: 'contribution_link_id',
|
||||
type: 'int',
|
||||
unsigned: true,
|
||||
nullable: true,
|
||||
default: null,
|
||||
})
|
||||
contributionLinkId?: number | null
|
||||
|
||||
@Column({ name: 'publisher_id', default: 0 })
|
||||
publisherId: number
|
||||
|
||||
@Column({ name: 'gms_allowed', type: 'bool', default: true })
|
||||
gmsAllowed: boolean
|
||||
|
||||
@Column({
|
||||
name: 'location',
|
||||
type: 'geometry',
|
||||
default: null,
|
||||
nullable: true,
|
||||
transformer: GeometryTransformer,
|
||||
})
|
||||
location: Geometry | null
|
||||
|
||||
@Column({
|
||||
name: 'gms_publish_location',
|
||||
type: 'int',
|
||||
unsigned: true,
|
||||
nullable: false,
|
||||
default: 2,
|
||||
})
|
||||
gmsPublishLocation: number
|
||||
|
||||
@Column({ name: 'gms_registered', type: 'bool', default: false })
|
||||
gmsRegistered: boolean
|
||||
|
||||
@Column({ name: 'gms_registered_at', type: 'datetime', default: null, nullable: true })
|
||||
gmsRegisteredAt: Date | null
|
||||
|
||||
@Column({ name: 'humhub_allowed', type: 'bool', default: false })
|
||||
humhubAllowed: boolean
|
||||
|
||||
@OneToMany(() => Contribution, (contribution) => contribution.user)
|
||||
@JoinColumn({ name: 'user_id' })
|
||||
contributions?: Contribution[]
|
||||
|
||||
@OneToMany(() => ContributionMessage, (message) => message.user)
|
||||
@JoinColumn({ name: 'user_id' })
|
||||
messages?: ContributionMessage[]
|
||||
|
||||
@OneToMany(() => UserContact, (userContact: UserContact) => userContact.user)
|
||||
@JoinColumn({ name: 'user_id' })
|
||||
userContacts?: UserContact[]
|
||||
|
||||
@OneToOne(() => DltTransaction, (dlt) => dlt.userId)
|
||||
@JoinColumn({ name: 'id', referencedColumnName: 'userId' })
|
||||
dltTransaction?: DltTransaction | null
|
||||
|
||||
@OneToOne(() => TransactionLink, (transactionLink) => transactionLink.userId)
|
||||
@JoinColumn({ name: 'id', referencedColumnName: 'userId' })
|
||||
transactionLink?: TransactionLink | null
|
||||
}
|
||||
1
database/entity/DltTransaction.ts
Normal file
1
database/entity/DltTransaction.ts
Normal file
@ -0,0 +1 @@
|
||||
export { DltTransaction } from './0089-merge_dlt_tables/DltTransaction'
|
||||
1
database/entity/Transaction.ts
Normal file
1
database/entity/Transaction.ts
Normal file
@ -0,0 +1 @@
|
||||
export { Transaction } from './0089-merge_dlt_tables/Transaction'
|
||||
1
database/entity/TransactionLink.ts
Normal file
1
database/entity/TransactionLink.ts
Normal file
@ -0,0 +1 @@
|
||||
export { TransactionLink } from './0089-merge_dlt_tables/TransactionLink'
|
||||
1
database/entity/User.ts
Normal file
1
database/entity/User.ts
Normal file
@ -0,0 +1 @@
|
||||
export { User } from './0089-merge_dlt_tables/User'
|
||||
30
database/migration/migrations/0091-add_dlt_users_table.ts
Normal file
30
database/migration/migrations/0091-add_dlt_users_table.ts
Normal file
@ -0,0 +1,30 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
|
||||
await queryFn(`
|
||||
CREATE TABLE \`dlt_users\` (
|
||||
\`id\` int unsigned NOT NULL AUTO_INCREMENT,
|
||||
\`user_id\` int(10) unsigned NOT NULL,
|
||||
\`message_id\` varchar(64) NULL DEFAULT NULL,
|
||||
\`verified\` tinyint(4) NOT NULL DEFAULT 0,
|
||||
\`created_at\` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
\`verified_at\` datetime(3),
|
||||
\`error\` text NULL DEFAULT NULL,
|
||||
PRIMARY KEY (id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;`)
|
||||
|
||||
await queryFn(
|
||||
'ALTER TABLE `dlt_transactions` RENAME COLUMN `transactions_id` TO `transaction_id`;',
|
||||
)
|
||||
await queryFn('ALTER TABLE `dlt_transactions` ADD COLUMN `error` text NULL DEFAULT NULL;')
|
||||
}
|
||||
|
||||
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
|
||||
await queryFn(`DROP TABLE \`dlt_users\`;`)
|
||||
|
||||
await queryFn(
|
||||
'ALTER TABLE `dlt_transactions` RENAME COLUMN `transaction_id` TO `transactions_id`;',
|
||||
)
|
||||
await queryFn('ALTER TABLE `dlt_transactions` DROP COLUMN `error`;')
|
||||
}
|
||||
37
database/migration/migrations/0092-merge_dlt_tables.ts
Normal file
37
database/migration/migrations/0092-merge_dlt_tables.ts
Normal file
@ -0,0 +1,37 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
|
||||
await queryFn(`DROP TABLE \`dlt_users\`;`)
|
||||
await queryFn(`
|
||||
ALTER TABLE \`dlt_transactions\`
|
||||
CHANGE \`transaction_id\` \`transaction_id\` INT(10) UNSIGNED NULL DEFAULT NULL,
|
||||
ADD \`user_id\` INT UNSIGNED NULL DEFAULT NULL AFTER \`transaction_id\`,
|
||||
ADD \`transaction_link_id\` INT UNSIGNED NULL DEFAULT NULL AFTER \`user_id\`,
|
||||
ADD \`type_id\` INT UNSIGNED NOT NULL AFTER \`transaction_link_id\`
|
||||
;
|
||||
`)
|
||||
}
|
||||
|
||||
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
|
||||
await queryFn(`
|
||||
CREATE TABLE \`dlt_users\` (
|
||||
\`id\` int unsigned NOT NULL AUTO_INCREMENT,
|
||||
\`user_id\` int(10) unsigned NOT NULL,
|
||||
\`message_id\` varchar(64) NULL DEFAULT NULL,
|
||||
\`verified\` tinyint(4) NOT NULL DEFAULT 0,
|
||||
\`created_at\` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
\`verified_at\` datetime(3),
|
||||
\`error\` text NULL DEFAULT NULL,
|
||||
PRIMARY KEY (id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;`)
|
||||
|
||||
await queryFn(`
|
||||
ALTER TABLE \`dlt_transactions\`
|
||||
CHANGE \`transaction_id\` \`transaction_id\` INT(10) UNSIGNED NOT NULL,
|
||||
DROP COLUMN \`user_id\`,
|
||||
DROP COLUMN \`transaction_link_id\`
|
||||
DROP COLUMN \`type_id\`
|
||||
;
|
||||
`)
|
||||
}
|
||||
@ -1,13 +1,24 @@
|
||||
import { BaseEntity, Column, Entity, JoinColumn, OneToOne, PrimaryGeneratedColumn } from 'typeorm'
|
||||
import { Transaction } from './Transaction'
|
||||
import { User } from './User'
|
||||
import { TransactionLink } from './TransactionLink'
|
||||
|
||||
@Entity('dlt_transactions', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' })
|
||||
export class DltTransaction extends BaseEntity {
|
||||
@PrimaryGeneratedColumn('increment', { unsigned: true })
|
||||
id: number
|
||||
|
||||
@Column({ name: 'transactions_id', type: 'int', unsigned: true, nullable: false })
|
||||
transactionId: number
|
||||
@Column({ name: 'transaction_id', type: 'int', unsigned: true, nullable: true })
|
||||
transactionId?: number | null
|
||||
|
||||
@Column({ name: 'user_id', type: 'int', unsigned: true, nullable: true })
|
||||
userId?: number | null
|
||||
|
||||
@Column({ name: 'transaction_link_id', type: 'int', unsigned: true, nullable: true })
|
||||
transactionLinkId?: number | null
|
||||
|
||||
@Column({ name: 'type_id', type: 'int', unsigned: true, nullable: false })
|
||||
typeId: number
|
||||
|
||||
@Column({
|
||||
name: 'message_id',
|
||||
@ -34,10 +45,18 @@ export class DltTransaction extends BaseEntity {
|
||||
@Column({ name: 'verified_at', type: 'datetime', precision: 3, nullable: true, default: null })
|
||||
verifiedAt: Date | null
|
||||
|
||||
@OneToOne(
|
||||
() => Transaction,
|
||||
(transaction) => transaction.dltTransaction,
|
||||
)
|
||||
@JoinColumn({ name: 'transactions_id' })
|
||||
@Column({ name: 'error', type: 'text', nullable: true })
|
||||
error: string | null
|
||||
|
||||
@OneToOne(() => Transaction, (transaction) => transaction.dltTransaction)
|
||||
@JoinColumn({ name: 'transaction_id' })
|
||||
transaction?: Transaction | null
|
||||
|
||||
@OneToOne(() => User, (user) => user.dltTransaction)
|
||||
@JoinColumn({ name: 'user_id' })
|
||||
user?: User | null
|
||||
|
||||
@OneToOne(() => TransactionLink, (transactionLink) => transactionLink.dltTransaction)
|
||||
@JoinColumn({ name: 'transaction_link_id' })
|
||||
transactionLink?: TransactionLink | null
|
||||
}
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
/* eslint-disable no-use-before-define */
|
||||
import { Decimal } from 'decimal.js-light'
|
||||
import { BaseEntity, Column, Entity, JoinColumn, OneToOne, PrimaryGeneratedColumn } from 'typeorm'
|
||||
import { BaseEntity, Column, Entity, JoinColumn, ManyToOne, OneToOne, PrimaryGeneratedColumn } from 'typeorm'
|
||||
import { Contribution } from './Contribution'
|
||||
import { DltTransaction } from './DltTransaction'
|
||||
import { DecimalTransformer } from './transformer/DecimalTransformer'
|
||||
import { TransactionLink } from './TransactionLink'
|
||||
|
||||
@Entity('transactions')
|
||||
export class Transaction extends BaseEntity {
|
||||
@ -158,14 +159,15 @@ export class Transaction extends BaseEntity {
|
||||
@JoinColumn({ name: 'id', referencedColumnName: 'transactionId' })
|
||||
contribution?: Contribution | null
|
||||
|
||||
@OneToOne(
|
||||
() => DltTransaction,
|
||||
(dlt) => dlt.transactionId,
|
||||
)
|
||||
@OneToOne(() => DltTransaction, (dlt) => dlt.transactionId)
|
||||
@JoinColumn({ name: 'id', referencedColumnName: 'transactionId' })
|
||||
dltTransaction?: DltTransaction | null
|
||||
|
||||
@OneToOne(() => Transaction)
|
||||
@JoinColumn({ name: 'previous' })
|
||||
previousTransaction?: Transaction | null
|
||||
|
||||
@ManyToOne(() => TransactionLink, (transactionLink) => transactionLink.transactions)
|
||||
@JoinColumn({ name: 'transaction_link_id' })
|
||||
transactionLink?: TransactionLink | null
|
||||
}
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
import { Decimal } from 'decimal.js-light'
|
||||
import { BaseEntity, Column, DeleteDateColumn, Entity, PrimaryGeneratedColumn } from 'typeorm'
|
||||
import { BaseEntity, Column, DeleteDateColumn, Entity, JoinColumn, OneToMany, OneToOne, PrimaryGeneratedColumn } from 'typeorm'
|
||||
import { DecimalTransformer } from './transformer/DecimalTransformer'
|
||||
import { User } from './User'
|
||||
import { DltTransaction } from './DltTransaction'
|
||||
import { Transaction } from './Transaction'
|
||||
|
||||
@Entity('transaction_links')
|
||||
export class TransactionLink extends BaseEntity {
|
||||
@ -58,4 +61,16 @@ export class TransactionLink extends BaseEntity {
|
||||
|
||||
@Column({ type: 'int', unsigned: true, nullable: true })
|
||||
redeemedBy: number | null
|
||||
|
||||
@OneToOne(() => DltTransaction, (dlt) => dlt.transactionLinkId)
|
||||
@JoinColumn({ name: 'id', referencedColumnName: 'transactionLinkId' })
|
||||
dltTransaction?: DltTransaction | null
|
||||
|
||||
@OneToOne(() => User, (user) => user.transactionLink)
|
||||
@JoinColumn({ name: 'userId' })
|
||||
user: User
|
||||
|
||||
@OneToMany(() => Transaction, (transaction) => transaction.transactionLink)
|
||||
@JoinColumn({ referencedColumnName: 'transaction_link_id' })
|
||||
transactions: Transaction[]
|
||||
}
|
||||
|
||||
@ -16,6 +16,8 @@ import { ContributionMessage } from './ContributionMessage'
|
||||
import { UserContact } from './UserContact'
|
||||
import { UserRole } from './UserRole'
|
||||
import { GeometryTransformer } from './transformer/GeometryTransformer'
|
||||
import { DltTransaction } from './DltTransaction'
|
||||
import { TransactionLink } from './TransactionLink'
|
||||
|
||||
@Entity('users', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' })
|
||||
export class User extends BaseEntity {
|
||||
@ -213,4 +215,12 @@ export class User extends BaseEntity {
|
||||
)
|
||||
@JoinColumn({ name: 'user_id' })
|
||||
userContacts?: UserContact[]
|
||||
|
||||
@OneToOne(() => DltTransaction, (dlt) => dlt.userId)
|
||||
@JoinColumn({ name: 'id', referencedColumnName: 'userId' })
|
||||
dltTransaction?: DltTransaction | null
|
||||
|
||||
@OneToOne(() => TransactionLink, (transactionLink) => transactionLink.userId)
|
||||
@JoinColumn({ name: 'id', referencedColumnName: 'userId' })
|
||||
transactionLink?: TransactionLink | null
|
||||
}
|
||||
|
||||
34
database/src/logging/TransactionLinkLogging.view.ts
Normal file
34
database/src/logging/TransactionLinkLogging.view.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import { TransactionLink } from '../entity/TransactionLink'
|
||||
import { AbstractLoggingView } from './AbstractLogging.view'
|
||||
import { DltTransactionLoggingView } from './DltTransactionLogging.view'
|
||||
import { TransactionLoggingView } from './TransactionLogging.view'
|
||||
import { UserLoggingView } from './UserLogging.view'
|
||||
|
||||
export class TransactionLinkLoggingView extends AbstractLoggingView {
|
||||
public constructor(private self: TransactionLink) {
|
||||
super()
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
public toJSON(): any {
|
||||
return {
|
||||
id: this.self.id,
|
||||
userId: this.self.userId,
|
||||
amount: this.decimalToString(this.self.amount),
|
||||
holdAvailableAmount: this.decimalToString(this.self.holdAvailableAmount),
|
||||
memoLength: this.self.memo.length,
|
||||
createdAt: this.dateToString(this.self.createdAt),
|
||||
deletedAt: this.dateToString(this.self.deletedAt),
|
||||
validUntil: this.dateToString(this.self.validUntil),
|
||||
redeemedAt: this.dateToString(this.self.redeemedAt),
|
||||
redeemedBy: this.self.redeemedBy,
|
||||
dltTransaction: this.self.dltTransaction
|
||||
? new DltTransactionLoggingView(this.self.dltTransaction).toJSON()
|
||||
: undefined,
|
||||
user: this.self.user ? new UserLoggingView(this.self.user).toJSON() : undefined,
|
||||
transactions: this.self.transactions.forEach(
|
||||
(transaction) => new TransactionLoggingView(transaction),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2,6 +2,7 @@ import { Transaction } from '../entity'
|
||||
import { AbstractLoggingView } from './AbstractLogging.view'
|
||||
import { ContributionLoggingView } from './ContributionLogging.view'
|
||||
import { DltTransactionLoggingView } from './DltTransactionLogging.view'
|
||||
import { TransactionLinkLoggingView } from './TransactionLinkLogging.view'
|
||||
|
||||
// TODO: move enum into database, maybe rename database
|
||||
enum TransactionTypeId {
|
||||
@ -41,7 +42,7 @@ export class TransactionLoggingView extends AbstractLoggingView {
|
||||
linkedUserName: this.self.linkedUserName?.substring(0, 3) + '...',
|
||||
linkedTransactionId: this.self.linkedTransactionId,
|
||||
contribution: this.self.contribution
|
||||
? new ContributionLoggingView(this.self.contribution)
|
||||
? new ContributionLoggingView(this.self.contribution).toJSON()
|
||||
: undefined,
|
||||
dltTransaction: this.self.dltTransaction
|
||||
? new DltTransactionLoggingView(this.self.dltTransaction).toJSON()
|
||||
@ -49,6 +50,9 @@ export class TransactionLoggingView extends AbstractLoggingView {
|
||||
previousTransaction: this.self.previousTransaction
|
||||
? new TransactionLoggingView(this.self.previousTransaction).toJSON()
|
||||
: undefined,
|
||||
transactionLink: this.self.transactionLink
|
||||
? new TransactionLinkLoggingView(this.self.transactionLink).toJSON()
|
||||
: undefined,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,18 +9,16 @@ IOTA_API_URL=https://chrysalis-nodes.iota.org
|
||||
IOTA_COMMUNITY_ALIAS=GRADIDO: TestHelloWelt2
|
||||
IOTA_HOME_COMMUNITY_SEED=aabbccddeeff00112233445566778899aabbccddeeff00112233445566778899
|
||||
|
||||
# Database
|
||||
DB_HOST=localhost
|
||||
DB_PORT=3306
|
||||
DB_USER=root
|
||||
DB_PASSWORD=
|
||||
DB_DATABASE=gradido_dlt
|
||||
DB_DATABASE_TEST=gradido_dlt_test
|
||||
TYPEORM_LOGGING_RELATIVE_PATH=typeorm.backend.log
|
||||
|
||||
# DLT-Connector
|
||||
DLT_CONNECTOR_PORT=6010
|
||||
|
||||
# Gradido Node Server URL
|
||||
NODE_SERVER_URL=http://localhost:8340
|
||||
|
||||
# Gradido Blockchain
|
||||
GRADIDO_BLOCKCHAIN_CRYPTO_APP_SECRET=21ffbbc616fe
|
||||
GRADIDO_BLOCKCHAIN_SERVER_CRYPTO_KEY=a51ef8ac7ef1abf162fb7a65261acd7a
|
||||
|
||||
# Route to Backend
|
||||
BACKEND_SERVER_URL=http://localhost:4000
|
||||
JWT_SECRET=secret123
|
||||
@ -7,17 +7,15 @@ IOTA_API_URL=$IOTA_API_URL
|
||||
IOTA_COMMUNITY_ALIAS=$IOTA_COMMUNITY_ALIAS
|
||||
IOTA_HOME_COMMUNITY_SEED=$IOTA_HOME_COMMUNITY_SEED
|
||||
|
||||
# Database
|
||||
DB_HOST=localhost
|
||||
DB_PORT=3306
|
||||
DB_USER=root
|
||||
DB_PASSWORD=
|
||||
DB_DATABASE=gradido_dlt
|
||||
DB_DATABASE_TEST=$DB_DATABASE_TEST
|
||||
TYPEORM_LOGGING_RELATIVE_PATH=typeorm.backend.log
|
||||
|
||||
# DLT-Connector
|
||||
DLT_CONNECTOR_PORT=$DLT_CONNECTOR_PORT
|
||||
|
||||
# Gradido Node Server URL
|
||||
NODE_SERVER_URL=$NODE_SERVER_URL
|
||||
|
||||
# Gradido Blockchain
|
||||
GRADIDO_BLOCKCHAIN_CRYPTO_APP_SECRET=$GRADIDO_BLOCKCHAIN_CRYPTO_APP_SECRET
|
||||
GRADIDO_BLOCKCHAIN_SERVER_CRYPTO_KEY=$GRADIDO_BLOCKCHAIN_SERVER_CRYPTO_KEY
|
||||
|
||||
# Route to Backend
|
||||
BACKEND_SERVER_URL=http://localhost:4000
|
||||
@ -22,18 +22,7 @@ module.exports = {
|
||||
'@input/(.*)': '<rootDir>/src/graphql/input/$1',
|
||||
'@proto/(.*)': '<rootDir>/src/proto/$1',
|
||||
'@test/(.*)': '<rootDir>/test/$1',
|
||||
'@typeorm/(.*)': '<rootDir>/src/typeorm/$1',
|
||||
'@client/(.*)': '<rootDir>/src/client/$1',
|
||||
'@entity/(.*)':
|
||||
// eslint-disable-next-line n/no-process-env
|
||||
process.env.NODE_ENV === 'development'
|
||||
? '<rootDir>/../dlt-database/entity/$1'
|
||||
: '<rootDir>/../dlt-database/build/entity/$1',
|
||||
'@dbTools/(.*)':
|
||||
// eslint-disable-next-line n/no-process-env
|
||||
process.env.NODE_ENV === 'development'
|
||||
? '<rootDir>/../dlt-database/src/$1'
|
||||
: '<rootDir>/../dlt-database/build/src/$1',
|
||||
'@validator/(.*)': '<rootDir>/src/graphql/validator/$1',
|
||||
},
|
||||
}
|
||||
|
||||
@ -13,33 +13,29 @@
|
||||
"start": "cross-env TZ=UTC TS_NODE_BASEURL=./build node -r tsconfig-paths/register build/src/index.js",
|
||||
"dev": "cross-env TZ=UTC nodemon -w src --ext ts --exec ts-node -r dotenv/config -r tsconfig-paths/register src/index.ts",
|
||||
"lint": "eslint --max-warnings=0 --ext .js,.ts .",
|
||||
"test": "cross-env TZ=UTC NODE_ENV=development jest --runInBand --forceExit --detectOpenHandles"
|
||||
"test": "cross-env TZ=UTC NODE_ENV=development jest --forceExit --detectOpenHandles"
|
||||
},
|
||||
"dependencies": {
|
||||
"@apollo/server": "^4.7.5",
|
||||
"@apollo/utils.fetcher": "^3.0.0",
|
||||
"@iota/client": "^2.2.4",
|
||||
"bip32-ed25519": "^0.0.4",
|
||||
"bip39": "^3.1.0",
|
||||
"body-parser": "^1.20.2",
|
||||
"class-validator": "^0.14.0",
|
||||
"cors": "^2.8.5",
|
||||
"cross-env": "^7.0.3",
|
||||
"decimal.js-light": "^2.5.1",
|
||||
"dlt-database": "file:../dlt-database",
|
||||
"dotenv": "10.0.0",
|
||||
"express": "4.17.1",
|
||||
"express-slow-down": "^2.0.1",
|
||||
"gradido-blockchain-js": "git+https://github.com/gradido/gradido-blockchain-js#1c75576",
|
||||
"graphql": "^16.7.1",
|
||||
"graphql-request": "^6.1.0",
|
||||
"graphql-scalars": "^1.22.2",
|
||||
"helmet": "^7.1.0",
|
||||
"jose": "^5.2.2",
|
||||
"jsonrpc-ts-client": "^0.2.3",
|
||||
"log4js": "^6.7.1",
|
||||
"nodemon": "^2.0.20",
|
||||
"protobufjs": "^7.2.5",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"sodium-native": "^4.0.4",
|
||||
"tsconfig-paths": "^4.1.2",
|
||||
"type-graphql": "^2.0.0-beta.2",
|
||||
"uuid": "^9.0.1"
|
||||
@ -69,8 +65,6 @@
|
||||
"prettier": "^2.8.7",
|
||||
"ts-jest": "^27.0.5",
|
||||
"ts-node": "^10.9.1",
|
||||
"typeorm": "^0.3.17",
|
||||
"typeorm-extension": "^3.0.1",
|
||||
"typescript": "^4.9.4"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
156
dlt-connector/src/client/GradidoNode.ts
Normal file
156
dlt-connector/src/client/GradidoNode.ts
Normal file
@ -0,0 +1,156 @@
|
||||
/* eslint-disable camelcase */
|
||||
import { AddressType, ConfirmedTransaction, MemoryBlock, stringToAddressType } from 'gradido-blockchain-js'
|
||||
import JsonRpcClient from 'jsonrpc-ts-client'
|
||||
import { JsonRpcEitherResponse } from 'jsonrpc-ts-client/dist/types/utils/jsonrpc'
|
||||
|
||||
import { CONFIG } from '@/config'
|
||||
import { logger } from '@/logging/logger'
|
||||
import { LogError } from '@/server/LogError'
|
||||
import { confirmedTransactionFromBase64 } from '@/utils/typeConverter'
|
||||
|
||||
const client = new JsonRpcClient({
|
||||
url: CONFIG.NODE_SERVER_URL,
|
||||
})
|
||||
/*
|
||||
enum JsonRPCErrorCodes {
|
||||
NONE = 0,
|
||||
GRADIDO_NODE_ERROR = -10000,
|
||||
UNKNOWN_GROUP = -10001,
|
||||
NOT_IMPLEMENTED = -10002,
|
||||
TRANSACTION_NOT_FOUND = -10003,
|
||||
// default errors from json rpc standard: https://www.jsonrpc.org/specification
|
||||
// -32700 Parse error Invalid JSON was received by the server.
|
||||
PARSE_ERROR = -32700,
|
||||
// -32600 Invalid Request The JSON sent is not a valid Request object.
|
||||
INVALID_REQUEST = -32600,
|
||||
// -32601 Method not found The method does not exist / is not available.
|
||||
METHODE_NOT_FOUND = -32601,
|
||||
// -32602 Invalid params Invalid method parameter(s).
|
||||
INVALID_PARAMS = -32602,
|
||||
// -32603 Internal error Internal JSON - RPC error.
|
||||
INTERNAL_ERROR = -32603,
|
||||
// -32000 to -32099 Server error Reserved for implementation-defined server-errors.
|
||||
}
|
||||
*/
|
||||
|
||||
interface ConfirmedTransactionList {
|
||||
transactions: string[]
|
||||
timeUsed: string
|
||||
}
|
||||
|
||||
interface ConfirmedTransactionResponse {
|
||||
transaction: string
|
||||
timeUsed: string
|
||||
}
|
||||
|
||||
interface AddressTypeResult {
|
||||
addressType: string
|
||||
}
|
||||
|
||||
function resolveResponse<T, R>(response: JsonRpcEitherResponse<T>, onSuccess: (result: T) => R): R {
|
||||
if (response.isSuccess()) {
|
||||
return onSuccess(response.result)
|
||||
} else if (response.isError()) {
|
||||
throw new LogError('error by json rpc request to gradido node server', response.error)
|
||||
}
|
||||
throw new LogError('no success and no error', response)
|
||||
}
|
||||
|
||||
async function getTransactions(
|
||||
fromTransactionId: number,
|
||||
maxResultCount: number,
|
||||
iotaTopic: string,
|
||||
): Promise<ConfirmedTransaction[]> {
|
||||
const parameter = {
|
||||
format: 'base64',
|
||||
fromTransactionId,
|
||||
maxResultCount,
|
||||
communityId: iotaTopic,
|
||||
}
|
||||
logger.info('call getTransactions on Node Server via jsonrpc 2.0 with ', parameter)
|
||||
const response = await client.exec<ConfirmedTransactionList>('getTransactions', parameter) // sends payload {jsonrpc: '2.0', params: ...}
|
||||
return resolveResponse(response, (result: ConfirmedTransactionList) => {
|
||||
logger.debug('GradidoNode used time', result.timeUsed)
|
||||
return result.transactions.map((transactionBase64) =>
|
||||
confirmedTransactionFromBase64(transactionBase64),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
async function getTransaction(
|
||||
transactionId: number | Buffer,
|
||||
iotaTopic: string,
|
||||
): Promise<ConfirmedTransaction | undefined> {
|
||||
logger.info('call gettransaction on Node Server via jsonrpc 2.0')
|
||||
const response = await client.exec<ConfirmedTransactionResponse>('gettransaction', {
|
||||
format: 'base64',
|
||||
communityId: iotaTopic,
|
||||
transactionId: typeof transactionId === 'number' ? transactionId : undefined,
|
||||
iotaMessageId: transactionId instanceof Buffer ? transactionId.toString('hex') : undefined,
|
||||
})
|
||||
return resolveResponse(response, (result: ConfirmedTransactionResponse) => {
|
||||
logger.debug('GradidoNode used time', result.timeUsed)
|
||||
return result.transaction && result.transaction !== ''
|
||||
? confirmedTransactionFromBase64(result.transaction)
|
||||
: undefined
|
||||
})
|
||||
}
|
||||
|
||||
async function getLastTransaction(iotaTopic: string): Promise<ConfirmedTransaction | undefined> {
|
||||
logger.info('call getlasttransaction on Node Server via jsonrpc 2.0')
|
||||
const response = await client.exec<ConfirmedTransactionResponse>('getlasttransaction', {
|
||||
format: 'base64',
|
||||
communityId: iotaTopic,
|
||||
})
|
||||
return resolveResponse(response, (result: ConfirmedTransactionResponse) => {
|
||||
logger.debug('GradidoNode used time', result.timeUsed)
|
||||
return result.transaction && result.transaction !== ''
|
||||
? confirmedTransactionFromBase64(result.transaction)
|
||||
: undefined
|
||||
})
|
||||
}
|
||||
|
||||
async function getAddressType(pubkey: Buffer, iotaTopic: string): Promise<AddressType | undefined> {
|
||||
logger.info('call getaddresstype on Node Server via jsonrpc 2.0')
|
||||
const response = await client.exec<AddressTypeResult>('getaddresstype', {
|
||||
pubkey: pubkey.toString('hex'),
|
||||
communityId: iotaTopic,
|
||||
})
|
||||
return resolveResponse(response, (result: AddressTypeResult) =>
|
||||
stringToAddressType(result.addressType),
|
||||
)
|
||||
}
|
||||
|
||||
async function getTransactionsForAccount(
|
||||
pubkey: MemoryBlock,
|
||||
iotaTopic: string,
|
||||
maxResultCount = 0,
|
||||
firstTransactionNr = 1,
|
||||
): Promise<ConfirmedTransaction[] | undefined> {
|
||||
const parameter = {
|
||||
pubkey: pubkey.convertToHex(),
|
||||
format: 'base64',
|
||||
firstTransactionNr,
|
||||
maxResultCount,
|
||||
communityId: iotaTopic,
|
||||
}
|
||||
logger.info('call listtransactionsforaddress on Node Server via jsonrpc 2.0', parameter)
|
||||
const response = await client.exec<ConfirmedTransactionList>(
|
||||
'listtransactionsforaddress',
|
||||
parameter,
|
||||
)
|
||||
return resolveResponse(response, (result: ConfirmedTransactionList) => {
|
||||
logger.debug('GradidoNode used time', result.timeUsed)
|
||||
return result.transactions.map((transactionBase64) =>
|
||||
confirmedTransactionFromBase64(transactionBase64),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
export {
|
||||
getTransaction,
|
||||
getLastTransaction,
|
||||
getTransactions,
|
||||
getAddressType,
|
||||
getTransactionsForAccount,
|
||||
}
|
||||
@ -4,12 +4,11 @@ dotenv.config()
|
||||
|
||||
const constants = {
|
||||
LOG4JS_CONFIG: 'log4js-config.json',
|
||||
DB_VERSION: '0003-refactor_transaction_recipe',
|
||||
// default log level on production should be info
|
||||
LOG_LEVEL: process.env.LOG_LEVEL ?? 'info',
|
||||
CONFIG_VERSION: {
|
||||
DEFAULT: 'DEFAULT',
|
||||
EXPECTED: 'v6.2024-02-20',
|
||||
EXPECTED: 'v7.2024-09-24',
|
||||
CURRENT: '',
|
||||
},
|
||||
}
|
||||
@ -19,26 +18,27 @@ const server = {
|
||||
JWT_SECRET: process.env.JWT_SECRET ?? 'secret123',
|
||||
}
|
||||
|
||||
const database = {
|
||||
DB_HOST: process.env.DB_HOST ?? 'localhost',
|
||||
DB_PORT: process.env.DB_PORT ? parseInt(process.env.DB_PORT) : 3306,
|
||||
DB_USER: process.env.DB_USER ?? 'root',
|
||||
DB_PASSWORD: process.env.DB_PASSWORD ?? '',
|
||||
DB_DATABASE: process.env.DB_DATABASE ?? 'gradido_dlt',
|
||||
DB_DATABASE_TEST: process.env.DB_DATABASE_TEST ?? 'gradido_dlt_test',
|
||||
TYPEORM_LOGGING_RELATIVE_PATH: process.env.TYPEORM_LOGGING_RELATIVE_PATH ?? 'typeorm.backend.log',
|
||||
}
|
||||
|
||||
const iota = {
|
||||
IOTA_API_URL: process.env.IOTA_API_URL ?? 'https://chrysalis-nodes.iota.org',
|
||||
IOTA_COMMUNITY_ALIAS: process.env.IOTA_COMMUNITY_ALIAS ?? 'GRADIDO: TestHelloWelt2',
|
||||
IOTA_HOME_COMMUNITY_SEED: process.env.IOTA_HOME_COMMUNITY_SEED?.substring(0, 32) ?? null,
|
||||
IOTA_HOME_COMMUNITY_SEED: process.env.IOTA_HOME_COMMUNITY_SEED ?? null,
|
||||
}
|
||||
|
||||
const dltConnector = {
|
||||
DLT_CONNECTOR_PORT: process.env.DLT_CONNECTOR_PORT ?? 6010,
|
||||
}
|
||||
|
||||
const nodeServer = {
|
||||
NODE_SERVER_URL: process.env.NODE_SERVER_URL ?? 'http://localhost:8340',
|
||||
}
|
||||
|
||||
const gradidoBlockchain = {
|
||||
GRADIDO_BLOCKCHAIN_CRYPTO_APP_SECRET:
|
||||
process.env.GRADIDO_BLOCKCHAIN_CRYPTO_APP_SECRET ?? 'invalid',
|
||||
GRADIDO_BLOCKCHAIN_SERVER_CRYPTO_KEY:
|
||||
process.env.GRADIDO_BLOCKCHAIN_SERVER_CRYPTO_KEY ?? 'invalid',
|
||||
}
|
||||
|
||||
const backendServer = {
|
||||
BACKEND_SERVER_URL: process.env.BACKEND_SERVER_URL ?? 'http://backend:4000',
|
||||
}
|
||||
@ -58,8 +58,9 @@ if (
|
||||
export const CONFIG = {
|
||||
...constants,
|
||||
...server,
|
||||
...database,
|
||||
...iota,
|
||||
...dltConnector,
|
||||
...nodeServer,
|
||||
...gradidoBlockchain,
|
||||
...backendServer,
|
||||
}
|
||||
|
||||
@ -1,60 +0,0 @@
|
||||
import { Account } from '@entity/Account'
|
||||
import Decimal from 'decimal.js-light'
|
||||
|
||||
import { KeyPair } from '@/data/KeyPair'
|
||||
import { AddressType } from '@/data/proto/3_3/enum/AddressType'
|
||||
import { UserAccountDraft } from '@/graphql/input/UserAccountDraft'
|
||||
import { hardenDerivationIndex } from '@/utils/derivationHelper'
|
||||
import { accountTypeToAddressType } from '@/utils/typeConverter'
|
||||
|
||||
const GMW_ACCOUNT_DERIVATION_INDEX = 1
|
||||
const AUF_ACCOUNT_DERIVATION_INDEX = 2
|
||||
|
||||
export class AccountFactory {
|
||||
public static createAccount(
|
||||
createdAt: Date,
|
||||
derivationIndex: number,
|
||||
type: AddressType,
|
||||
parentKeyPair: KeyPair,
|
||||
): Account {
|
||||
const account = Account.create()
|
||||
account.derivationIndex = derivationIndex
|
||||
account.derive2Pubkey = parentKeyPair.derive([derivationIndex]).publicKey
|
||||
account.type = type.valueOf()
|
||||
account.createdAt = createdAt
|
||||
account.balanceOnConfirmation = new Decimal(0)
|
||||
account.balanceOnCreation = new Decimal(0)
|
||||
account.balanceCreatedAt = createdAt
|
||||
return account
|
||||
}
|
||||
|
||||
public static createAccountFromUserAccountDraft(
|
||||
{ createdAt, accountType, user }: UserAccountDraft,
|
||||
parentKeyPair: KeyPair,
|
||||
): Account {
|
||||
return AccountFactory.createAccount(
|
||||
new Date(createdAt),
|
||||
user.accountNr ?? 1,
|
||||
accountTypeToAddressType(accountType),
|
||||
parentKeyPair,
|
||||
)
|
||||
}
|
||||
|
||||
public static createGmwAccount(keyPair: KeyPair, createdAt: Date): Account {
|
||||
return AccountFactory.createAccount(
|
||||
createdAt,
|
||||
hardenDerivationIndex(GMW_ACCOUNT_DERIVATION_INDEX),
|
||||
AddressType.COMMUNITY_GMW,
|
||||
keyPair,
|
||||
)
|
||||
}
|
||||
|
||||
public static createAufAccount(keyPair: KeyPair, createdAt: Date): Account {
|
||||
return AccountFactory.createAccount(
|
||||
createdAt,
|
||||
hardenDerivationIndex(AUF_ACCOUNT_DERIVATION_INDEX),
|
||||
AddressType.COMMUNITY_AUF,
|
||||
keyPair,
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -1,35 +0,0 @@
|
||||
import { Account } from '@entity/Account'
|
||||
|
||||
import { LogError } from '@/server/LogError'
|
||||
|
||||
import { KeyPair } from './KeyPair'
|
||||
import { UserLogic } from './User.logic'
|
||||
|
||||
export class AccountLogic {
|
||||
// eslint-disable-next-line no-useless-constructor
|
||||
public constructor(private self: Account) {}
|
||||
|
||||
/**
|
||||
* calculate account key pair starting from community key pair => derive user key pair => derive account key pair
|
||||
* @param communityKeyPair
|
||||
*/
|
||||
public calculateKeyPair(communityKeyPair: KeyPair): KeyPair {
|
||||
if (!this.self.user) {
|
||||
throw new LogError('missing user')
|
||||
}
|
||||
const userLogic = new UserLogic(this.self.user)
|
||||
const accountKeyPair = userLogic
|
||||
.calculateKeyPair(communityKeyPair)
|
||||
.derive([this.self.derivationIndex])
|
||||
|
||||
if (
|
||||
this.self.derive2Pubkey &&
|
||||
this.self.derive2Pubkey.compare(accountKeyPair.publicKey) !== 0
|
||||
) {
|
||||
throw new LogError(
|
||||
'The freshly derived public key does not correspond to the stored public key',
|
||||
)
|
||||
}
|
||||
return accountKeyPair
|
||||
}
|
||||
}
|
||||
@ -1,34 +0,0 @@
|
||||
import { Account } from '@entity/Account'
|
||||
import { User } from '@entity/User'
|
||||
import { In } from 'typeorm'
|
||||
|
||||
import { UserIdentifier } from '@/graphql/input/UserIdentifier'
|
||||
import { getDataSource } from '@/typeorm/DataSource'
|
||||
|
||||
export const AccountRepository = getDataSource()
|
||||
.getRepository(Account)
|
||||
.extend({
|
||||
findAccountsByPublicKeys(publicKeys: Buffer[]): Promise<Account[]> {
|
||||
return this.findBy({ derive2Pubkey: In(publicKeys) })
|
||||
},
|
||||
|
||||
async findAccountByPublicKey(publicKey: Buffer | undefined): Promise<Account | undefined> {
|
||||
if (!publicKey) return undefined
|
||||
return (await this.findOneBy({ derive2Pubkey: Buffer.from(publicKey) })) ?? undefined
|
||||
},
|
||||
|
||||
async findAccountByUserIdentifier({
|
||||
uuid,
|
||||
accountNr,
|
||||
}: UserIdentifier): Promise<Account | undefined> {
|
||||
const user = await User.findOne({
|
||||
where: { gradidoID: uuid, accounts: { derivationIndex: accountNr ?? 1 } },
|
||||
relations: { accounts: true },
|
||||
})
|
||||
if (user && user.accounts?.length === 1) {
|
||||
const account = user.accounts[0]
|
||||
account.user = user
|
||||
return account
|
||||
}
|
||||
},
|
||||
})
|
||||
@ -1,197 +0,0 @@
|
||||
import 'reflect-metadata'
|
||||
import { Decimal } from 'decimal.js-light'
|
||||
|
||||
import { TestDB } from '@test/TestDB'
|
||||
|
||||
import { AccountType } from '@/graphql/enum/AccountType'
|
||||
import { UserAccountDraft } from '@/graphql/input/UserAccountDraft'
|
||||
import { UserIdentifier } from '@/graphql/input/UserIdentifier'
|
||||
|
||||
import { AccountFactory } from './Account.factory'
|
||||
import { AccountRepository } from './Account.repository'
|
||||
import { KeyPair } from './KeyPair'
|
||||
import { Mnemonic } from './Mnemonic'
|
||||
import { AddressType } from './proto/3_3/enum/AddressType'
|
||||
import { UserFactory } from './User.factory'
|
||||
import { UserLogic } from './User.logic'
|
||||
|
||||
const con = TestDB.instance
|
||||
|
||||
jest.mock('@typeorm/DataSource', () => ({
|
||||
getDataSource: jest.fn(() => TestDB.instance.dbConnect),
|
||||
}))
|
||||
|
||||
describe('data/Account test factory and repository', () => {
|
||||
const now = new Date()
|
||||
const keyPair1 = new KeyPair(new Mnemonic('62ef251edc2416f162cd24ab1711982b'))
|
||||
const keyPair2 = new KeyPair(new Mnemonic('000a0000000002000000000003000070'))
|
||||
const keyPair3 = new KeyPair(new Mnemonic('00ba541a1000020000000000300bda70'))
|
||||
const userGradidoID = '6be949ab-8198-4acf-ba63-740089081d61'
|
||||
|
||||
describe('test factory methods', () => {
|
||||
beforeAll(async () => {
|
||||
await con.setupTestDB()
|
||||
})
|
||||
afterAll(async () => {
|
||||
await con.teardownTestDB()
|
||||
})
|
||||
|
||||
it('test createAccount', () => {
|
||||
const account = AccountFactory.createAccount(now, 1, AddressType.COMMUNITY_HUMAN, keyPair1)
|
||||
expect(account).toMatchObject({
|
||||
derivationIndex: 1,
|
||||
derive2Pubkey: Buffer.from(
|
||||
'cb88043ef4833afc01d6ed9b34e1aa48e79dce5ff97c07090c6600ec05f6d994',
|
||||
'hex',
|
||||
),
|
||||
type: AddressType.COMMUNITY_HUMAN,
|
||||
createdAt: now,
|
||||
balanceCreatedAt: now,
|
||||
balanceOnConfirmation: new Decimal(0),
|
||||
balanceOnCreation: new Decimal(0),
|
||||
})
|
||||
})
|
||||
|
||||
it('test createAccountFromUserAccountDraft', () => {
|
||||
const userAccountDraft = new UserAccountDraft()
|
||||
userAccountDraft.createdAt = now.toISOString()
|
||||
userAccountDraft.accountType = AccountType.COMMUNITY_HUMAN
|
||||
userAccountDraft.user = new UserIdentifier()
|
||||
userAccountDraft.user.accountNr = 1
|
||||
const account = AccountFactory.createAccountFromUserAccountDraft(userAccountDraft, keyPair1)
|
||||
expect(account).toMatchObject({
|
||||
derivationIndex: 1,
|
||||
derive2Pubkey: Buffer.from(
|
||||
'cb88043ef4833afc01d6ed9b34e1aa48e79dce5ff97c07090c6600ec05f6d994',
|
||||
'hex',
|
||||
),
|
||||
type: AddressType.COMMUNITY_HUMAN,
|
||||
createdAt: now,
|
||||
balanceCreatedAt: now,
|
||||
balanceOnConfirmation: new Decimal(0),
|
||||
balanceOnCreation: new Decimal(0),
|
||||
})
|
||||
})
|
||||
|
||||
it('test createGmwAccount', () => {
|
||||
const account = AccountFactory.createGmwAccount(keyPair1, now)
|
||||
expect(account).toMatchObject({
|
||||
derivationIndex: 2147483649,
|
||||
derive2Pubkey: Buffer.from(
|
||||
'05f0060357bb73bd290283870fc47a10b3764f02ca26938479ed853f46145366',
|
||||
'hex',
|
||||
),
|
||||
type: AddressType.COMMUNITY_GMW,
|
||||
createdAt: now,
|
||||
balanceCreatedAt: now,
|
||||
balanceOnConfirmation: new Decimal(0),
|
||||
balanceOnCreation: new Decimal(0),
|
||||
})
|
||||
})
|
||||
|
||||
it('test createAufAccount', () => {
|
||||
const account = AccountFactory.createAufAccount(keyPair1, now)
|
||||
expect(account).toMatchObject({
|
||||
derivationIndex: 2147483650,
|
||||
derive2Pubkey: Buffer.from(
|
||||
'6c749f8693a4a58c948e5ae54df11e2db33d2f98673b56e0cf19c0132614ab59',
|
||||
'hex',
|
||||
),
|
||||
type: AddressType.COMMUNITY_AUF,
|
||||
createdAt: now,
|
||||
balanceCreatedAt: now,
|
||||
balanceOnConfirmation: new Decimal(0),
|
||||
balanceOnCreation: new Decimal(0),
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('test repository functions', () => {
|
||||
beforeAll(async () => {
|
||||
await con.setupTestDB()
|
||||
await Promise.all([
|
||||
AccountFactory.createAufAccount(keyPair1, now).save(),
|
||||
AccountFactory.createGmwAccount(keyPair1, now).save(),
|
||||
AccountFactory.createAufAccount(keyPair2, now).save(),
|
||||
AccountFactory.createGmwAccount(keyPair2, now).save(),
|
||||
AccountFactory.createAufAccount(keyPair3, now).save(),
|
||||
AccountFactory.createGmwAccount(keyPair3, now).save(),
|
||||
])
|
||||
const userAccountDraft = new UserAccountDraft()
|
||||
userAccountDraft.accountType = AccountType.COMMUNITY_HUMAN
|
||||
userAccountDraft.createdAt = now.toString()
|
||||
userAccountDraft.user = new UserIdentifier()
|
||||
userAccountDraft.user.accountNr = 1
|
||||
userAccountDraft.user.uuid = userGradidoID
|
||||
const user = UserFactory.create(userAccountDraft, keyPair1)
|
||||
const userLogic = new UserLogic(user)
|
||||
const account = AccountFactory.createAccountFromUserAccountDraft(
|
||||
userAccountDraft,
|
||||
userLogic.calculateKeyPair(keyPair1),
|
||||
)
|
||||
account.user = user
|
||||
// user is set to cascade: ['insert'] will be saved together with account
|
||||
await account.save()
|
||||
})
|
||||
afterAll(async () => {
|
||||
await con.teardownTestDB()
|
||||
})
|
||||
it('test findAccountsByPublicKeys', async () => {
|
||||
const accounts = await AccountRepository.findAccountsByPublicKeys([
|
||||
Buffer.from('6c749f8693a4a58c948e5ae54df11e2db33d2f98673b56e0cf19c0132614ab59', 'hex'),
|
||||
Buffer.from('0fa996b73b624592fe326b8500cb1e3f10026112b374d84c87d097f4d489c019', 'hex'),
|
||||
Buffer.from('0ffa996b73b624592f26b850b0cb1e3f1026112b374d84c87d017f4d489c0197', 'hex'), // invalid
|
||||
])
|
||||
expect(accounts).toHaveLength(2)
|
||||
expect(accounts).toMatchObject(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
derivationIndex: 2147483649,
|
||||
derive2Pubkey: Buffer.from(
|
||||
'0fa996b73b624592fe326b8500cb1e3f10026112b374d84c87d097f4d489c019',
|
||||
'hex',
|
||||
),
|
||||
type: AddressType.COMMUNITY_GMW,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
derivationIndex: 2147483650,
|
||||
derive2Pubkey: Buffer.from(
|
||||
'6c749f8693a4a58c948e5ae54df11e2db33d2f98673b56e0cf19c0132614ab59',
|
||||
'hex',
|
||||
),
|
||||
type: AddressType.COMMUNITY_AUF,
|
||||
}),
|
||||
]),
|
||||
)
|
||||
})
|
||||
|
||||
it('test findAccountByPublicKey', async () => {
|
||||
expect(
|
||||
await AccountRepository.findAccountByPublicKey(
|
||||
Buffer.from('6c749f8693a4a58c948e5ae54df11e2db33d2f98673b56e0cf19c0132614ab59', 'hex'),
|
||||
),
|
||||
).toMatchObject({
|
||||
derivationIndex: 2147483650,
|
||||
derive2Pubkey: Buffer.from(
|
||||
'6c749f8693a4a58c948e5ae54df11e2db33d2f98673b56e0cf19c0132614ab59',
|
||||
'hex',
|
||||
),
|
||||
type: AddressType.COMMUNITY_AUF,
|
||||
})
|
||||
})
|
||||
|
||||
it('test findAccountByUserIdentifier', async () => {
|
||||
const userIdentifier = new UserIdentifier()
|
||||
userIdentifier.accountNr = 1
|
||||
userIdentifier.uuid = userGradidoID
|
||||
expect(await AccountRepository.findAccountByUserIdentifier(userIdentifier)).toMatchObject({
|
||||
derivationIndex: 1,
|
||||
derive2Pubkey: Buffer.from(
|
||||
'2099c004a26e5387c9fbbc9bb0f552a9642d3fd7c710ae5802b775d24ff36f93',
|
||||
'hex',
|
||||
),
|
||||
type: AddressType.COMMUNITY_HUMAN,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1,13 +0,0 @@
|
||||
import { BackendTransaction } from '@entity/BackendTransaction'
|
||||
|
||||
import { TransactionDraft } from '@/graphql/input/TransactionDraft'
|
||||
|
||||
export class BackendTransactionFactory {
|
||||
public static createFromTransactionDraft(transactionDraft: TransactionDraft): BackendTransaction {
|
||||
const backendTransaction = BackendTransaction.create()
|
||||
backendTransaction.backendTransactionId = transactionDraft.backendTransactionId
|
||||
backendTransaction.typeId = transactionDraft.type
|
||||
backendTransaction.createdAt = new Date(transactionDraft.createdAt)
|
||||
return backendTransaction
|
||||
}
|
||||
}
|
||||
@ -1,7 +0,0 @@
|
||||
import { BackendTransaction } from '@entity/BackendTransaction'
|
||||
|
||||
import { getDataSource } from '@/typeorm/DataSource'
|
||||
|
||||
export const BackendTransactionRepository = getDataSource()
|
||||
.getRepository(BackendTransaction)
|
||||
.extend({})
|
||||
@ -1,76 +0,0 @@
|
||||
import { Community } from '@entity/Community'
|
||||
import { FindOptionsSelect, In, IsNull, Not } from 'typeorm'
|
||||
|
||||
import { CommunityArg } from '@/graphql/arg/CommunityArg'
|
||||
import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType'
|
||||
import { CommunityDraft } from '@/graphql/input/CommunityDraft'
|
||||
import { UserIdentifier } from '@/graphql/input/UserIdentifier'
|
||||
import { TransactionError } from '@/graphql/model/TransactionError'
|
||||
import { LogError } from '@/server/LogError'
|
||||
import { getDataSource } from '@/typeorm/DataSource'
|
||||
import { iotaTopicFromCommunityUUID } from '@/utils/typeConverter'
|
||||
|
||||
import { KeyPair } from './KeyPair'
|
||||
|
||||
export const CommunityRepository = getDataSource()
|
||||
.getRepository(Community)
|
||||
.extend({
|
||||
async isExist(community: CommunityDraft | string): Promise<boolean> {
|
||||
const iotaTopic =
|
||||
community instanceof CommunityDraft ? iotaTopicFromCommunityUUID(community.uuid) : community
|
||||
const result = await this.find({
|
||||
where: { iotaTopic },
|
||||
})
|
||||
return result.length > 0
|
||||
},
|
||||
|
||||
async findByCommunityArg({ uuid, foreign, confirmed }: CommunityArg): Promise<Community[]> {
|
||||
return await this.find({
|
||||
where: {
|
||||
...(uuid && { iotaTopic: iotaTopicFromCommunityUUID(uuid) }),
|
||||
...(foreign && { foreign }),
|
||||
...(confirmed && { confirmedAt: Not(IsNull()) }),
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
async findByCommunityUuid(communityUuid: string): Promise<Community | null> {
|
||||
return await this.findOneBy({ iotaTopic: iotaTopicFromCommunityUUID(communityUuid) })
|
||||
},
|
||||
|
||||
async findByIotaTopic(iotaTopic: string): Promise<Community | null> {
|
||||
return await this.findOneBy({ iotaTopic })
|
||||
},
|
||||
|
||||
findCommunitiesByTopics(topics: string[]): Promise<Community[]> {
|
||||
return this.findBy({ iotaTopic: In(topics) })
|
||||
},
|
||||
|
||||
async getCommunityForUserIdentifier(
|
||||
identifier: UserIdentifier,
|
||||
): Promise<Community | undefined> {
|
||||
if (!identifier.communityUuid) {
|
||||
throw new TransactionError(TransactionErrorType.MISSING_PARAMETER, 'community uuid not set')
|
||||
}
|
||||
return (
|
||||
(await this.findOneBy({
|
||||
iotaTopic: iotaTopicFromCommunityUUID(identifier.communityUuid),
|
||||
})) ?? undefined
|
||||
)
|
||||
},
|
||||
|
||||
findAll(select: FindOptionsSelect<Community>): Promise<Community[]> {
|
||||
return this.find({ select })
|
||||
},
|
||||
|
||||
async loadHomeCommunityKeyPair(): Promise<KeyPair> {
|
||||
const community = await this.findOneOrFail({
|
||||
where: { foreign: false },
|
||||
select: { rootChaincode: true, rootPubkey: true, rootPrivkey: true },
|
||||
})
|
||||
if (!community.rootChaincode || !community.rootPrivkey) {
|
||||
throw new LogError('Missing chaincode or private key for home community')
|
||||
}
|
||||
return new KeyPair(community)
|
||||
},
|
||||
})
|
||||
@ -1,88 +0,0 @@
|
||||
import { Community } from '@entity/Community'
|
||||
|
||||
// https://www.npmjs.com/package/bip32-ed25519
|
||||
import { LogError } from '@/server/LogError'
|
||||
|
||||
import { toPublic, derivePrivate, sign, verify, generateFromSeed } from 'bip32-ed25519'
|
||||
|
||||
import { Mnemonic } from './Mnemonic'
|
||||
import { SignaturePair } from './proto/3_3/SignaturePair'
|
||||
|
||||
/**
|
||||
* Class Managing Key Pair and also generate, sign and verify signature with it
|
||||
*/
|
||||
export class KeyPair {
|
||||
private _publicKey: Buffer
|
||||
private _chainCode: Buffer
|
||||
private _privateKey: Buffer
|
||||
|
||||
/**
|
||||
* @param input: Mnemonic = Mnemonic or Passphrase which work as seed for generating algorithms
|
||||
* @param input: Buffer = extended private key, returned from bip32-ed25519 generateFromSeed or from derivePrivate
|
||||
* @param input: Community = community entity with keys loaded from db
|
||||
*
|
||||
*/
|
||||
public constructor(input: Mnemonic | Buffer | Community) {
|
||||
if (input instanceof Mnemonic) {
|
||||
this.loadFromExtendedPrivateKey(generateFromSeed(input.seed))
|
||||
} else if (input instanceof Buffer) {
|
||||
this.loadFromExtendedPrivateKey(input)
|
||||
} else if (input instanceof Community) {
|
||||
if (!input.rootPrivkey || !input.rootChaincode || !input.rootPubkey) {
|
||||
throw new LogError('missing private key or chaincode or public key in commmunity entity')
|
||||
}
|
||||
this._privateKey = input.rootPrivkey
|
||||
this._publicKey = input.rootPubkey
|
||||
this._chainCode = input.rootChaincode
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* copy keys to community entity
|
||||
* @param community
|
||||
*/
|
||||
public fillInCommunityKeys(community: Community) {
|
||||
community.rootPubkey = this._publicKey
|
||||
community.rootPrivkey = this._privateKey
|
||||
community.rootChaincode = this._chainCode
|
||||
}
|
||||
|
||||
private loadFromExtendedPrivateKey(extendedPrivateKey: Buffer) {
|
||||
if (extendedPrivateKey.length !== 96) {
|
||||
throw new LogError('invalid extended private key')
|
||||
}
|
||||
this._privateKey = extendedPrivateKey.subarray(0, 64)
|
||||
this._chainCode = extendedPrivateKey.subarray(64, 96)
|
||||
this._publicKey = toPublic(extendedPrivateKey).subarray(0, 32)
|
||||
}
|
||||
|
||||
public getExtendPrivateKey(): Buffer {
|
||||
return Buffer.concat([this._privateKey, this._chainCode])
|
||||
}
|
||||
|
||||
public getExtendPublicKey(): Buffer {
|
||||
return Buffer.concat([this._publicKey, this._chainCode])
|
||||
}
|
||||
|
||||
public get publicKey(): Buffer {
|
||||
return this._publicKey
|
||||
}
|
||||
|
||||
public derive(path: number[]): KeyPair {
|
||||
const extendedPrivateKey = this.getExtendPrivateKey()
|
||||
return new KeyPair(
|
||||
path.reduce(
|
||||
(extendPrivateKey: Buffer, node: number) => derivePrivate(extendPrivateKey, node),
|
||||
extendedPrivateKey,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
public sign(message: Buffer): Buffer {
|
||||
return sign(message, this.getExtendPrivateKey())
|
||||
}
|
||||
|
||||
public static verify(message: Buffer, { signature, pubKey }: SignaturePair): boolean {
|
||||
return verify(message, signature, pubKey)
|
||||
}
|
||||
}
|
||||
72
dlt-connector/src/data/KeyPairIdentifier.ts
Normal file
72
dlt-connector/src/data/KeyPairIdentifier.ts
Normal file
@ -0,0 +1,72 @@
|
||||
import { UserIdentifier } from '@/graphql/input/UserIdentifier'
|
||||
import { LogError } from '@/server/LogError'
|
||||
import { uuid4sToMemoryBlock } from '@/utils/typeConverter'
|
||||
|
||||
export class KeyPairIdentifier {
|
||||
// used for community key pair if it is only parameter or for user key pair
|
||||
communityUuid?: string
|
||||
// if set calculate key pair from seed, ignore all other parameter
|
||||
seed?: string
|
||||
// used for user key pair and account key pair, need also communityUuid
|
||||
userUuid?: string
|
||||
// used for account key pair, need also userUuid
|
||||
accountNr?: number
|
||||
|
||||
public constructor(input: UserIdentifier | string | undefined = undefined) {
|
||||
if (input instanceof UserIdentifier) {
|
||||
if (input.seed !== undefined) {
|
||||
this.seed = input.seed.seed
|
||||
} else {
|
||||
this.communityUuid = input.communityUuid
|
||||
this.userUuid = input.communityUser?.uuid
|
||||
this.accountNr = input.communityUser?.accountNr
|
||||
}
|
||||
} else if (typeof input === 'string') {
|
||||
this.communityUuid = input
|
||||
}
|
||||
}
|
||||
|
||||
isCommunityKeyPair(): boolean {
|
||||
return this.communityUuid !== undefined && this.userUuid === undefined
|
||||
}
|
||||
|
||||
isSeedKeyPair(): boolean {
|
||||
return this.seed !== undefined
|
||||
}
|
||||
|
||||
isUserKeyPair(): boolean {
|
||||
return (
|
||||
this.communityUuid !== undefined &&
|
||||
this.userUuid !== undefined &&
|
||||
this.accountNr === undefined
|
||||
)
|
||||
}
|
||||
|
||||
isAccountKeyPair(): boolean {
|
||||
return (
|
||||
this.communityUuid !== undefined &&
|
||||
this.userUuid !== undefined &&
|
||||
this.accountNr !== undefined
|
||||
)
|
||||
}
|
||||
|
||||
getKey(): string {
|
||||
if (this.seed && this.isSeedKeyPair()) {
|
||||
return this.seed
|
||||
} else if (this.communityUuid && this.isCommunityKeyPair()) {
|
||||
return this.communityUuid
|
||||
}
|
||||
if (this.userUuid && this.communityUuid) {
|
||||
const communityUserHash = uuid4sToMemoryBlock([this.userUuid, this.communityUuid])
|
||||
.calculateHash()
|
||||
.convertToHex()
|
||||
if (this.isUserKeyPair()) {
|
||||
return communityUserHash
|
||||
}
|
||||
if (this.accountNr && this.isAccountKeyPair()) {
|
||||
return communityUserHash + this.accountNr.toString()
|
||||
}
|
||||
}
|
||||
throw new LogError('KeyPairIdentifier: unhandled input type', this)
|
||||
}
|
||||
}
|
||||
@ -1,48 +0,0 @@
|
||||
// https://www.npmjs.com/package/bip39
|
||||
import { entropyToMnemonic, mnemonicToSeedSync } from 'bip39'
|
||||
// eslint-disable-next-line camelcase
|
||||
import { randombytes_buf } from 'sodium-native'
|
||||
|
||||
import { LogError } from '@/server/LogError'
|
||||
|
||||
export class Mnemonic {
|
||||
private _passphrase = ''
|
||||
public constructor(seed?: Buffer | string) {
|
||||
if (seed) {
|
||||
Mnemonic.validateSeed(seed)
|
||||
this._passphrase = entropyToMnemonic(seed)
|
||||
return
|
||||
}
|
||||
const entropy = Buffer.alloc(256)
|
||||
randombytes_buf(entropy)
|
||||
this._passphrase = entropyToMnemonic(entropy)
|
||||
}
|
||||
|
||||
public get passphrase(): string {
|
||||
return this._passphrase
|
||||
}
|
||||
|
||||
public get seed(): Buffer {
|
||||
return mnemonicToSeedSync(this._passphrase)
|
||||
}
|
||||
|
||||
public static validateSeed(seed: Buffer | string): void {
|
||||
let seedBuffer: Buffer
|
||||
if (!Buffer.isBuffer(seed)) {
|
||||
seedBuffer = Buffer.from(seed, 'hex')
|
||||
} else {
|
||||
seedBuffer = seed
|
||||
}
|
||||
if (seedBuffer.length < 16 || seedBuffer.length > 32 || seedBuffer.length % 4 !== 0) {
|
||||
throw new LogError(
|
||||
'invalid seed, must be in binary between 16 and 32 Bytes, Power of 4, for more infos: https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki#generating-the-mnemonic',
|
||||
{
|
||||
seedBufferHex: seedBuffer.toString('hex'),
|
||||
toShort: seedBuffer.length < 16,
|
||||
toLong: seedBuffer.length > 32,
|
||||
powerOf4: seedBuffer.length % 4,
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,179 +0,0 @@
|
||||
import { Account } from '@entity/Account'
|
||||
import { Community } from '@entity/Community'
|
||||
import { Transaction } from '@entity/Transaction'
|
||||
|
||||
import { GradidoTransaction } from '@/data/proto/3_3/GradidoTransaction'
|
||||
import { TransactionBody } from '@/data/proto/3_3/TransactionBody'
|
||||
import { TransactionDraft } from '@/graphql/input/TransactionDraft'
|
||||
import { UserIdentifier } from '@/graphql/input/UserIdentifier'
|
||||
import { LogError } from '@/server/LogError'
|
||||
import { bodyBytesToTransactionBody, transactionBodyToBodyBytes } from '@/utils/typeConverter'
|
||||
|
||||
import { AccountRepository } from './Account.repository'
|
||||
import { BackendTransactionFactory } from './BackendTransaction.factory'
|
||||
import { CommunityRepository } from './Community.repository'
|
||||
import { TransactionBodyBuilder } from './proto/TransactionBody.builder'
|
||||
|
||||
export class TransactionBuilder {
|
||||
private transaction: Transaction
|
||||
|
||||
// https://refactoring.guru/design-patterns/builder/typescript/example
|
||||
/**
|
||||
* A fresh builder instance should contain a blank product object, which is
|
||||
* used in further assembly.
|
||||
*/
|
||||
constructor() {
|
||||
this.reset()
|
||||
}
|
||||
|
||||
public reset(): void {
|
||||
this.transaction = Transaction.create()
|
||||
}
|
||||
|
||||
/**
|
||||
* Concrete Builders are supposed to provide their own methods for
|
||||
* retrieving results. That's because various types of builders may create
|
||||
* entirely different products that don't follow the same interface.
|
||||
* Therefore, such methods cannot be declared in the base Builder interface
|
||||
* (at least in a statically typed programming language).
|
||||
*
|
||||
* Usually, after returning the end result to the client, a builder instance
|
||||
* is expected to be ready to start producing another product. That's why
|
||||
* it's a usual practice to call the reset method at the end of the
|
||||
* `getProduct` method body. However, this behavior is not mandatory, and
|
||||
* you can make your builders wait for an explicit reset call from the
|
||||
* client code before disposing of the previous result.
|
||||
*/
|
||||
public build(): Transaction {
|
||||
const result = this.transaction
|
||||
this.reset()
|
||||
return result
|
||||
}
|
||||
|
||||
// return transaction without calling reset
|
||||
public getTransaction(): Transaction {
|
||||
return this.transaction
|
||||
}
|
||||
|
||||
public getCommunity(): Community {
|
||||
return this.transaction.community
|
||||
}
|
||||
|
||||
public getOtherCommunity(): Community | undefined {
|
||||
return this.transaction.otherCommunity
|
||||
}
|
||||
|
||||
public setSigningAccount(signingAccount: Account): TransactionBuilder {
|
||||
this.transaction.signingAccount = signingAccount
|
||||
return this
|
||||
}
|
||||
|
||||
public setRecipientAccount(recipientAccount: Account): TransactionBuilder {
|
||||
this.transaction.recipientAccount = recipientAccount
|
||||
return this
|
||||
}
|
||||
|
||||
public setCommunity(community: Community): TransactionBuilder {
|
||||
this.transaction.community = community
|
||||
return this
|
||||
}
|
||||
|
||||
public setOtherCommunity(otherCommunity?: Community): TransactionBuilder {
|
||||
if (!this.transaction.community) {
|
||||
throw new LogError('Please set community first!')
|
||||
}
|
||||
|
||||
this.transaction.otherCommunity =
|
||||
otherCommunity &&
|
||||
this.transaction.community &&
|
||||
this.transaction.community.id !== otherCommunity.id
|
||||
? otherCommunity
|
||||
: undefined
|
||||
return this
|
||||
}
|
||||
|
||||
public setSignature(signature: Buffer): TransactionBuilder {
|
||||
this.transaction.signature = signature
|
||||
return this
|
||||
}
|
||||
|
||||
public addBackendTransaction(transactionDraft: TransactionDraft): TransactionBuilder {
|
||||
if (!this.transaction.backendTransactions) {
|
||||
this.transaction.backendTransactions = []
|
||||
}
|
||||
this.transaction.backendTransactions.push(
|
||||
BackendTransactionFactory.createFromTransactionDraft(transactionDraft),
|
||||
)
|
||||
return this
|
||||
}
|
||||
|
||||
public async setCommunityFromUser(user: UserIdentifier): Promise<TransactionBuilder> {
|
||||
// get sender community
|
||||
const community = await CommunityRepository.getCommunityForUserIdentifier(user)
|
||||
if (!community) {
|
||||
throw new LogError("couldn't find community for transaction")
|
||||
}
|
||||
return this.setCommunity(community)
|
||||
}
|
||||
|
||||
public async setOtherCommunityFromUser(user: UserIdentifier): Promise<TransactionBuilder> {
|
||||
// get recipient community
|
||||
const otherCommunity = await CommunityRepository.getCommunityForUserIdentifier(user)
|
||||
return this.setOtherCommunity(otherCommunity)
|
||||
}
|
||||
|
||||
public async fromGradidoTransactionSearchForAccounts(
|
||||
gradidoTransaction: GradidoTransaction,
|
||||
): Promise<TransactionBuilder> {
|
||||
this.transaction.bodyBytes = Buffer.from(gradidoTransaction.bodyBytes)
|
||||
const transactionBody = bodyBytesToTransactionBody(this.transaction.bodyBytes)
|
||||
this.fromTransactionBody(transactionBody)
|
||||
|
||||
const firstSigPair = gradidoTransaction.getFirstSignature()
|
||||
// TODO: adapt if transactions with more than one signatures where added
|
||||
|
||||
// get recipient and signer accounts if not already set
|
||||
this.transaction.signingAccount ??= await AccountRepository.findAccountByPublicKey(
|
||||
firstSigPair.pubKey,
|
||||
)
|
||||
this.transaction.recipientAccount ??= await AccountRepository.findAccountByPublicKey(
|
||||
transactionBody.getRecipientPublicKey(),
|
||||
)
|
||||
this.transaction.signature = Buffer.from(firstSigPair.signature)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public fromGradidoTransaction(gradidoTransaction: GradidoTransaction): TransactionBuilder {
|
||||
this.transaction.bodyBytes = Buffer.from(gradidoTransaction.bodyBytes)
|
||||
const transactionBody = bodyBytesToTransactionBody(this.transaction.bodyBytes)
|
||||
this.fromTransactionBody(transactionBody)
|
||||
|
||||
const firstSigPair = gradidoTransaction.getFirstSignature()
|
||||
// TODO: adapt if transactions with more than one signatures where added
|
||||
this.transaction.signature = Buffer.from(firstSigPair.signature)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public fromTransactionBody(transactionBody: TransactionBody): TransactionBuilder {
|
||||
transactionBody.fillTransactionRecipe(this.transaction)
|
||||
this.transaction.bodyBytes ??= transactionBodyToBodyBytes(transactionBody)
|
||||
return this
|
||||
}
|
||||
|
||||
public fromTransactionBodyBuilder(
|
||||
transactionBodyBuilder: TransactionBodyBuilder,
|
||||
): TransactionBuilder {
|
||||
const signingAccount = transactionBodyBuilder.getSigningAccount()
|
||||
if (signingAccount) {
|
||||
this.setSigningAccount(signingAccount)
|
||||
}
|
||||
const recipientAccount = transactionBodyBuilder.getRecipientAccount()
|
||||
if (recipientAccount) {
|
||||
this.setRecipientAccount(recipientAccount)
|
||||
}
|
||||
this.fromTransactionBody(transactionBodyBuilder.getTransactionBody())
|
||||
return this
|
||||
}
|
||||
}
|
||||
@ -1,323 +0,0 @@
|
||||
import { Community } from '@entity/Community'
|
||||
import { Transaction } from '@entity/Transaction'
|
||||
import { Decimal } from 'decimal.js-light'
|
||||
|
||||
import { logger } from '@/logging/logger'
|
||||
|
||||
import { CommunityRoot } from './proto/3_3/CommunityRoot'
|
||||
import { CrossGroupType } from './proto/3_3/enum/CrossGroupType'
|
||||
import { GradidoCreation } from './proto/3_3/GradidoCreation'
|
||||
import { GradidoDeferredTransfer } from './proto/3_3/GradidoDeferredTransfer'
|
||||
import { GradidoTransfer } from './proto/3_3/GradidoTransfer'
|
||||
import { RegisterAddress } from './proto/3_3/RegisterAddress'
|
||||
import { TransactionBody } from './proto/3_3/TransactionBody'
|
||||
import { TransactionLogic } from './Transaction.logic'
|
||||
|
||||
let a: Transaction
|
||||
let b: Transaction
|
||||
|
||||
describe('data/transaction.logic', () => {
|
||||
describe('isBelongTogether', () => {
|
||||
beforeEach(() => {
|
||||
const now = new Date()
|
||||
let body = new TransactionBody()
|
||||
body.type = CrossGroupType.OUTBOUND
|
||||
body.transfer = new GradidoTransfer()
|
||||
body.otherGroup = 'recipient group'
|
||||
|
||||
a = new Transaction()
|
||||
a.community = new Community()
|
||||
a.communityId = 1
|
||||
a.otherCommunityId = 2
|
||||
a.id = 1
|
||||
a.signingAccountId = 1
|
||||
a.recipientAccountId = 2
|
||||
a.createdAt = now
|
||||
a.amount = new Decimal('100')
|
||||
a.signature = Buffer.alloc(64)
|
||||
a.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
|
||||
|
||||
body = new TransactionBody()
|
||||
body.type = CrossGroupType.INBOUND
|
||||
body.transfer = new GradidoTransfer()
|
||||
body.otherGroup = 'sending group'
|
||||
|
||||
b = new Transaction()
|
||||
b.community = new Community()
|
||||
b.communityId = 2
|
||||
b.otherCommunityId = 1
|
||||
b.id = 2
|
||||
b.signingAccountId = 1
|
||||
b.recipientAccountId = 2
|
||||
b.createdAt = now
|
||||
b.amount = new Decimal('100')
|
||||
b.signature = Buffer.alloc(64)
|
||||
b.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
|
||||
})
|
||||
|
||||
const spy = jest.spyOn(logger, 'info')
|
||||
|
||||
it('true', () => {
|
||||
const logic = new TransactionLogic(a)
|
||||
expect(logic.isBelongTogether(b)).toBe(true)
|
||||
})
|
||||
|
||||
it('false because of same id', () => {
|
||||
b.id = 1
|
||||
const logic = new TransactionLogic(a)
|
||||
expect(logic.isBelongTogether(b)).toBe(false)
|
||||
expect(spy).toHaveBeenLastCalledWith('id is the same, it is the same transaction!')
|
||||
})
|
||||
|
||||
it('false because of different signing accounts', () => {
|
||||
b.signingAccountId = 17
|
||||
const logic = new TransactionLogic(a)
|
||||
expect(logic.isBelongTogether(b)).toBe(false)
|
||||
expect(spy).toHaveBeenLastCalledWith(
|
||||
'transaction a and b are not pairs',
|
||||
expect.objectContaining({}),
|
||||
)
|
||||
})
|
||||
|
||||
it('false because of different recipient accounts', () => {
|
||||
b.recipientAccountId = 21
|
||||
const logic = new TransactionLogic(a)
|
||||
expect(logic.isBelongTogether(b)).toBe(false)
|
||||
expect(spy).toHaveBeenLastCalledWith(
|
||||
'transaction a and b are not pairs',
|
||||
expect.objectContaining({}),
|
||||
)
|
||||
})
|
||||
|
||||
it('false because of different community ids', () => {
|
||||
b.communityId = 6
|
||||
const logic = new TransactionLogic(a)
|
||||
expect(logic.isBelongTogether(b)).toBe(false)
|
||||
expect(spy).toHaveBeenLastCalledWith(
|
||||
'transaction a and b are not pairs',
|
||||
expect.objectContaining({}),
|
||||
)
|
||||
})
|
||||
|
||||
it('false because of different other community ids', () => {
|
||||
b.otherCommunityId = 3
|
||||
const logic = new TransactionLogic(a)
|
||||
expect(logic.isBelongTogether(b)).toBe(false)
|
||||
expect(spy).toHaveBeenLastCalledWith(
|
||||
'transaction a and b are not pairs',
|
||||
expect.objectContaining({}),
|
||||
)
|
||||
})
|
||||
|
||||
it('false because of different createdAt', () => {
|
||||
b.createdAt = new Date('2021-01-01T17:12')
|
||||
const logic = new TransactionLogic(a)
|
||||
expect(logic.isBelongTogether(b)).toBe(false)
|
||||
expect(spy).toHaveBeenLastCalledWith(
|
||||
'transaction a and b are not pairs',
|
||||
expect.objectContaining({}),
|
||||
)
|
||||
})
|
||||
|
||||
describe('false because of mismatching cross group type', () => {
|
||||
const body = new TransactionBody()
|
||||
it('a is LOCAL', () => {
|
||||
body.type = CrossGroupType.LOCAL
|
||||
a.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
|
||||
const logic = new TransactionLogic(a)
|
||||
expect(logic.isBelongTogether(b)).toBe(false)
|
||||
expect(spy).toHaveBeenNthCalledWith(7, 'no one can be LOCAL')
|
||||
expect(spy).toHaveBeenLastCalledWith(
|
||||
"cross group types don't match",
|
||||
expect.objectContaining({}),
|
||||
)
|
||||
})
|
||||
|
||||
it('b is LOCAL', () => {
|
||||
body.type = CrossGroupType.LOCAL
|
||||
b.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
|
||||
const logic = new TransactionLogic(a)
|
||||
expect(logic.isBelongTogether(b)).toBe(false)
|
||||
expect(spy).toHaveBeenNthCalledWith(9, 'no one can be LOCAL')
|
||||
expect(spy).toHaveBeenLastCalledWith(
|
||||
"cross group types don't match",
|
||||
expect.objectContaining({}),
|
||||
)
|
||||
})
|
||||
|
||||
it('both are INBOUND', () => {
|
||||
body.type = CrossGroupType.INBOUND
|
||||
a.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
|
||||
b.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
|
||||
const logic = new TransactionLogic(a)
|
||||
expect(logic.isBelongTogether(b)).toBe(false)
|
||||
expect(spy).toHaveBeenLastCalledWith(
|
||||
"cross group types don't match",
|
||||
expect.objectContaining({}),
|
||||
)
|
||||
})
|
||||
|
||||
it('both are OUTBOUND', () => {
|
||||
body.type = CrossGroupType.OUTBOUND
|
||||
a.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
|
||||
b.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
|
||||
const logic = new TransactionLogic(a)
|
||||
expect(logic.isBelongTogether(b)).toBe(false)
|
||||
expect(spy).toHaveBeenLastCalledWith(
|
||||
"cross group types don't match",
|
||||
expect.objectContaining({}),
|
||||
)
|
||||
})
|
||||
|
||||
it('a is CROSS', () => {
|
||||
body.type = CrossGroupType.CROSS
|
||||
a.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
|
||||
const logic = new TransactionLogic(a)
|
||||
expect(logic.isBelongTogether(b)).toBe(false)
|
||||
expect(spy).toHaveBeenLastCalledWith(
|
||||
"cross group types don't match",
|
||||
expect.objectContaining({}),
|
||||
)
|
||||
})
|
||||
|
||||
it('b is CROSS', () => {
|
||||
body.type = CrossGroupType.CROSS
|
||||
b.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
|
||||
const logic = new TransactionLogic(a)
|
||||
expect(logic.isBelongTogether(b)).toBe(false)
|
||||
expect(spy).toHaveBeenLastCalledWith(
|
||||
"cross group types don't match",
|
||||
expect.objectContaining({}),
|
||||
)
|
||||
})
|
||||
|
||||
it('true with a as INBOUND and b as OUTBOUND', () => {
|
||||
let body = TransactionBody.fromBodyBytes(a.bodyBytes)
|
||||
body.type = CrossGroupType.INBOUND
|
||||
a.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
|
||||
body = TransactionBody.fromBodyBytes(b.bodyBytes)
|
||||
body.type = CrossGroupType.OUTBOUND
|
||||
b.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
|
||||
const logic = new TransactionLogic(a)
|
||||
expect(logic.isBelongTogether(b)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('false because of transaction type not suitable for cross group transactions', () => {
|
||||
const body = new TransactionBody()
|
||||
body.type = CrossGroupType.OUTBOUND
|
||||
it('without transaction type (broken TransactionBody)', () => {
|
||||
a.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
|
||||
const logic = new TransactionLogic(a)
|
||||
expect(() => logic.isBelongTogether(b)).toThrowError("couldn't determine transaction type")
|
||||
})
|
||||
|
||||
it('not the same transaction types', () => {
|
||||
const body = new TransactionBody()
|
||||
body.type = CrossGroupType.OUTBOUND
|
||||
body.registerAddress = new RegisterAddress()
|
||||
a.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
|
||||
const logic = new TransactionLogic(a)
|
||||
expect(logic.isBelongTogether(b)).toBe(false)
|
||||
expect(spy).toHaveBeenLastCalledWith(
|
||||
"transaction types don't match",
|
||||
expect.objectContaining({}),
|
||||
)
|
||||
})
|
||||
|
||||
it('community root cannot be a cross group transaction', () => {
|
||||
let body = new TransactionBody()
|
||||
body.type = CrossGroupType.OUTBOUND
|
||||
body.communityRoot = new CommunityRoot()
|
||||
a.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
|
||||
body = new TransactionBody()
|
||||
body.type = CrossGroupType.INBOUND
|
||||
body.communityRoot = new CommunityRoot()
|
||||
b.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
|
||||
const logic = new TransactionLogic(a)
|
||||
expect(logic.isBelongTogether(b)).toBe(false)
|
||||
expect(spy).toHaveBeenLastCalledWith(
|
||||
"TransactionType COMMUNITY_ROOT couldn't be a CrossGroup Transaction",
|
||||
)
|
||||
})
|
||||
|
||||
it('Gradido Creation cannot be a cross group transaction', () => {
|
||||
let body = new TransactionBody()
|
||||
body.type = CrossGroupType.OUTBOUND
|
||||
body.creation = new GradidoCreation()
|
||||
a.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
|
||||
body = new TransactionBody()
|
||||
body.type = CrossGroupType.INBOUND
|
||||
body.creation = new GradidoCreation()
|
||||
b.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
|
||||
const logic = new TransactionLogic(a)
|
||||
expect(logic.isBelongTogether(b)).toBe(false)
|
||||
expect(spy).toHaveBeenLastCalledWith(
|
||||
"TransactionType GRADIDO_CREATION couldn't be a CrossGroup Transaction",
|
||||
)
|
||||
})
|
||||
|
||||
it('Deferred Transfer cannot be a cross group transaction', () => {
|
||||
let body = new TransactionBody()
|
||||
body.type = CrossGroupType.OUTBOUND
|
||||
body.deferredTransfer = new GradidoDeferredTransfer()
|
||||
a.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
|
||||
body = new TransactionBody()
|
||||
body.type = CrossGroupType.INBOUND
|
||||
body.deferredTransfer = new GradidoDeferredTransfer()
|
||||
b.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
|
||||
const logic = new TransactionLogic(a)
|
||||
expect(logic.isBelongTogether(b)).toBe(false)
|
||||
expect(spy).toHaveBeenLastCalledWith(
|
||||
"TransactionType GRADIDO_DEFERRED_TRANSFER couldn't be a CrossGroup Transaction",
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('false because of wrong amount', () => {
|
||||
it('amount missing on a', () => {
|
||||
a.amount = undefined
|
||||
const logic = new TransactionLogic(a)
|
||||
expect(logic.isBelongTogether(b)).toBe(false)
|
||||
expect(spy).toHaveBeenLastCalledWith('missing amount')
|
||||
})
|
||||
|
||||
it('amount missing on b', () => {
|
||||
b.amount = undefined
|
||||
const logic = new TransactionLogic(a)
|
||||
expect(logic.isBelongTogether(b)).toBe(false)
|
||||
expect(spy).toHaveBeenLastCalledWith('missing amount')
|
||||
})
|
||||
|
||||
it('amount not the same', () => {
|
||||
a.amount = new Decimal('101')
|
||||
const logic = new TransactionLogic(a)
|
||||
expect(logic.isBelongTogether(b)).toBe(false)
|
||||
expect(spy).toHaveBeenLastCalledWith('amounts mismatch', expect.objectContaining({}))
|
||||
})
|
||||
})
|
||||
|
||||
it('false because otherGroup are the same', () => {
|
||||
const body = new TransactionBody()
|
||||
body.type = CrossGroupType.OUTBOUND
|
||||
body.transfer = new GradidoTransfer()
|
||||
body.otherGroup = 'sending group'
|
||||
a.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
|
||||
const logic = new TransactionLogic(a)
|
||||
expect(logic.isBelongTogether(b)).toBe(false)
|
||||
expect(spy).toHaveBeenLastCalledWith('otherGroups are the same', expect.objectContaining({}))
|
||||
})
|
||||
|
||||
it('false because of different memos', () => {
|
||||
const body = new TransactionBody()
|
||||
body.type = CrossGroupType.OUTBOUND
|
||||
body.transfer = new GradidoTransfer()
|
||||
body.otherGroup = 'recipient group'
|
||||
body.memo = 'changed memo'
|
||||
a.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
|
||||
const logic = new TransactionLogic(a)
|
||||
expect(logic.isBelongTogether(b)).toBe(false)
|
||||
expect(spy).toHaveBeenLastCalledWith('memo differ', expect.objectContaining({}))
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1,200 +0,0 @@
|
||||
import { Transaction } from '@entity/Transaction'
|
||||
import { Not } from 'typeorm'
|
||||
|
||||
import { logger } from '@/logging/logger'
|
||||
import { TransactionBodyLoggingView } from '@/logging/TransactionBodyLogging.view'
|
||||
import { TransactionLoggingView } from '@/logging/TransactionLogging.view'
|
||||
import { LogError } from '@/server/LogError'
|
||||
|
||||
import { CrossGroupType } from './proto/3_3/enum/CrossGroupType'
|
||||
import { TransactionType } from './proto/3_3/enum/TransactionType'
|
||||
import { TransactionBody } from './proto/3_3/TransactionBody'
|
||||
|
||||
export class TransactionLogic {
|
||||
protected transactionBody: TransactionBody | undefined
|
||||
|
||||
// eslint-disable-next-line no-useless-constructor
|
||||
public constructor(private self: Transaction) {}
|
||||
|
||||
/**
|
||||
* search for transaction pair for Cross Group Transaction
|
||||
* @returns
|
||||
*/
|
||||
public async findPairTransaction(): Promise<Transaction> {
|
||||
const type = this.getBody().type
|
||||
if (type === CrossGroupType.LOCAL) {
|
||||
throw new LogError("local transaction don't has a pairing transaction")
|
||||
}
|
||||
|
||||
// check if already was loaded from db
|
||||
if (this.self.pairingTransaction) {
|
||||
return this.self.pairingTransaction
|
||||
}
|
||||
|
||||
if (this.self.pairingTransaction) {
|
||||
const pairingTransaction = await Transaction.findOneBy({ id: this.self.pairingTransaction })
|
||||
if (pairingTransaction) {
|
||||
return pairingTransaction
|
||||
}
|
||||
}
|
||||
// check if we find some in db
|
||||
const sameCreationDateTransactions = await Transaction.findBy({
|
||||
createdAt: this.self.createdAt,
|
||||
id: Not(this.self.id),
|
||||
})
|
||||
if (
|
||||
sameCreationDateTransactions.length === 1 &&
|
||||
this.isBelongTogether(sameCreationDateTransactions[0])
|
||||
) {
|
||||
return sameCreationDateTransactions[0]
|
||||
}
|
||||
// this approach only work if all entities get ids really incremented by one
|
||||
if (type === CrossGroupType.OUTBOUND) {
|
||||
const prevTransaction = await Transaction.findOneBy({ id: this.self.id - 1 })
|
||||
if (prevTransaction && this.isBelongTogether(prevTransaction)) {
|
||||
return prevTransaction
|
||||
}
|
||||
} else if (type === CrossGroupType.INBOUND) {
|
||||
const nextTransaction = await Transaction.findOneBy({ id: this.self.id + 1 })
|
||||
if (nextTransaction && this.isBelongTogether(nextTransaction)) {
|
||||
return nextTransaction
|
||||
}
|
||||
}
|
||||
throw new LogError("couldn't find valid pairing transaction", {
|
||||
id: this.self.id,
|
||||
type: CrossGroupType[type],
|
||||
transactionCountWithSameCreatedAt: sameCreationDateTransactions.length,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* check if two transactions belong together
|
||||
* are they pairs for a cross group transaction
|
||||
* @param otherTransaction
|
||||
*/
|
||||
public isBelongTogether(otherTransaction: Transaction): boolean {
|
||||
if (this.self.id === otherTransaction.id) {
|
||||
logger.info('id is the same, it is the same transaction!')
|
||||
return false
|
||||
}
|
||||
|
||||
if (
|
||||
this.self.signingAccountId !== otherTransaction.signingAccountId ||
|
||||
this.self.recipientAccountId !== otherTransaction.recipientAccountId ||
|
||||
this.self.communityId !== otherTransaction.otherCommunityId ||
|
||||
this.self.otherCommunityId !== otherTransaction.communityId ||
|
||||
this.self.createdAt.getTime() !== otherTransaction.createdAt.getTime()
|
||||
) {
|
||||
logger.info('transaction a and b are not pairs', {
|
||||
a: new TransactionLoggingView(this.self).toJSON(),
|
||||
b: new TransactionLoggingView(otherTransaction).toJSON(),
|
||||
})
|
||||
return false
|
||||
}
|
||||
const body = this.getBody()
|
||||
const otherBody = TransactionBody.fromBodyBytes(otherTransaction.bodyBytes)
|
||||
/**
|
||||
* both must be Cross or
|
||||
* one can be OUTBOUND and one can be INBOUND
|
||||
* no one can be LOCAL
|
||||
*/
|
||||
|
||||
if (!this.validCrossGroupTypes(body.type, otherBody.type)) {
|
||||
logger.info("cross group types don't match", {
|
||||
a: new TransactionBodyLoggingView(body).toJSON(),
|
||||
b: new TransactionBodyLoggingView(otherBody).toJSON(),
|
||||
})
|
||||
return false
|
||||
}
|
||||
const type = body.getTransactionType()
|
||||
const otherType = otherBody.getTransactionType()
|
||||
if (!type || !otherType) {
|
||||
throw new LogError("couldn't determine transaction type", {
|
||||
a: new TransactionBodyLoggingView(body).toJSON(),
|
||||
b: new TransactionBodyLoggingView(otherBody).toJSON(),
|
||||
})
|
||||
}
|
||||
if (type !== otherType) {
|
||||
logger.info("transaction types don't match", {
|
||||
a: new TransactionBodyLoggingView(body).toJSON(),
|
||||
b: new TransactionBodyLoggingView(otherBody).toJSON(),
|
||||
})
|
||||
return false
|
||||
}
|
||||
if (
|
||||
[
|
||||
TransactionType.COMMUNITY_ROOT,
|
||||
TransactionType.GRADIDO_CREATION,
|
||||
TransactionType.GRADIDO_DEFERRED_TRANSFER,
|
||||
].includes(type)
|
||||
) {
|
||||
logger.info(`TransactionType ${TransactionType[type]} couldn't be a CrossGroup Transaction`)
|
||||
return false
|
||||
}
|
||||
if (
|
||||
[
|
||||
TransactionType.GRADIDO_CREATION,
|
||||
TransactionType.GRADIDO_TRANSFER,
|
||||
TransactionType.GRADIDO_DEFERRED_TRANSFER,
|
||||
].includes(type)
|
||||
) {
|
||||
if (!this.self.amount || !otherTransaction.amount) {
|
||||
logger.info('missing amount')
|
||||
return false
|
||||
}
|
||||
if (this.self.amount.cmp(otherTransaction.amount.toString())) {
|
||||
logger.info('amounts mismatch', {
|
||||
a: this.self.amount.toString(),
|
||||
b: otherTransaction.amount.toString(),
|
||||
})
|
||||
return false
|
||||
}
|
||||
}
|
||||
if (body.otherGroup === otherBody.otherGroup) {
|
||||
logger.info('otherGroups are the same', {
|
||||
a: new TransactionBodyLoggingView(body).toJSON(),
|
||||
b: new TransactionBodyLoggingView(otherBody).toJSON(),
|
||||
})
|
||||
return false
|
||||
}
|
||||
if (body.memo !== otherBody.memo) {
|
||||
logger.info('memo differ', {
|
||||
a: new TransactionBodyLoggingView(body).toJSON(),
|
||||
b: new TransactionBodyLoggingView(otherBody).toJSON(),
|
||||
})
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* both must be CROSS or
|
||||
* one can be OUTBOUND and one can be INBOUND
|
||||
* no one can be LOCAL
|
||||
* @return true if crossGroupTypes are valid
|
||||
*/
|
||||
protected validCrossGroupTypes(a: CrossGroupType, b: CrossGroupType): boolean {
|
||||
logger.debug('compare ', {
|
||||
a: CrossGroupType[a],
|
||||
b: CrossGroupType[b],
|
||||
})
|
||||
if (a === CrossGroupType.LOCAL || b === CrossGroupType.LOCAL) {
|
||||
logger.info('no one can be LOCAL')
|
||||
return false
|
||||
}
|
||||
if (
|
||||
(a === CrossGroupType.INBOUND && b === CrossGroupType.OUTBOUND) ||
|
||||
(a === CrossGroupType.OUTBOUND && b === CrossGroupType.INBOUND)
|
||||
) {
|
||||
return true // One can be INBOUND and one can be OUTBOUND
|
||||
}
|
||||
return a === CrossGroupType.CROSS && b === CrossGroupType.CROSS
|
||||
}
|
||||
|
||||
public getBody(): TransactionBody {
|
||||
if (!this.transactionBody) {
|
||||
this.transactionBody = TransactionBody.fromBodyBytes(this.self.bodyBytes)
|
||||
}
|
||||
return this.transactionBody
|
||||
}
|
||||
}
|
||||
@ -1,43 +0,0 @@
|
||||
import { Transaction } from '@entity/Transaction'
|
||||
import { IsNull } from 'typeorm'
|
||||
|
||||
import { getDataSource } from '@/typeorm/DataSource'
|
||||
|
||||
// https://www.artima.com/articles/the-dci-architecture-a-new-vision-of-object-oriented-programming
|
||||
export const TransactionRepository = getDataSource()
|
||||
.getRepository(Transaction)
|
||||
.extend({
|
||||
findBySignature(signature: Buffer): Promise<Transaction | null> {
|
||||
return this.findOneBy({ signature: Buffer.from(signature) })
|
||||
},
|
||||
findByMessageId(iotaMessageId: string): Promise<Transaction | null> {
|
||||
return this.findOneBy({ iotaMessageId: Buffer.from(iotaMessageId, 'hex') })
|
||||
},
|
||||
async getNextPendingTransaction(): Promise<Transaction | null> {
|
||||
return await this.findOne({
|
||||
where: { iotaMessageId: IsNull() },
|
||||
order: { createdAt: 'ASC' },
|
||||
relations: { signingAccount: true },
|
||||
})
|
||||
},
|
||||
findExistingTransactionAndMissingMessageIds(messageIDsHex: string[]): Promise<Transaction[]> {
|
||||
return this.createQueryBuilder('Transaction')
|
||||
.where('HEX(Transaction.iota_message_id) IN (:...messageIDs)', {
|
||||
messageIDs: messageIDsHex,
|
||||
})
|
||||
.leftJoinAndSelect('Transaction.community', 'Community')
|
||||
.leftJoinAndSelect('Transaction.otherCommunity', 'OtherCommunity')
|
||||
.leftJoinAndSelect('Transaction.recipientAccount', 'RecipientAccount')
|
||||
.leftJoinAndSelect('Transaction.backendTransactions', 'BackendTransactions')
|
||||
.leftJoinAndSelect('RecipientAccount.user', 'RecipientUser')
|
||||
.leftJoinAndSelect('Transaction.signingAccount', 'SigningAccount')
|
||||
.leftJoinAndSelect('SigningAccount.user', 'SigningUser')
|
||||
.getMany()
|
||||
},
|
||||
removeConfirmedTransaction(transactions: Transaction[]): Transaction[] {
|
||||
return transactions.filter(
|
||||
(transaction: Transaction) =>
|
||||
transaction.runningHash === undefined || transaction.runningHash.length === 0,
|
||||
)
|
||||
},
|
||||
})
|
||||
@ -1,18 +0,0 @@
|
||||
import { User } from '@entity/User'
|
||||
|
||||
import { UserAccountDraft } from '@/graphql/input/UserAccountDraft'
|
||||
|
||||
import { KeyPair } from './KeyPair'
|
||||
import { UserLogic } from './User.logic'
|
||||
|
||||
export class UserFactory {
|
||||
static create(userAccountDraft: UserAccountDraft, parentKeys: KeyPair): User {
|
||||
const user = User.create()
|
||||
user.createdAt = new Date(userAccountDraft.createdAt)
|
||||
user.gradidoID = userAccountDraft.user.uuid
|
||||
const userLogic = new UserLogic(user)
|
||||
// store generated pubkey into entity
|
||||
userLogic.calculateKeyPair(parentKeys)
|
||||
return user
|
||||
}
|
||||
}
|
||||
@ -1,42 +0,0 @@
|
||||
import { User } from '@entity/User'
|
||||
|
||||
import { LogError } from '@/server/LogError'
|
||||
import { hardenDerivationIndex } from '@/utils/derivationHelper'
|
||||
import { uuid4ToBuffer } from '@/utils/typeConverter'
|
||||
|
||||
import { KeyPair } from './KeyPair'
|
||||
|
||||
export class UserLogic {
|
||||
// eslint-disable-next-line no-useless-constructor
|
||||
constructor(private user: User) {}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param parentKeys from home community for own user
|
||||
* @returns
|
||||
*/
|
||||
|
||||
calculateKeyPair = (parentKeys: KeyPair): KeyPair => {
|
||||
if (!this.user.gradidoID) {
|
||||
throw new LogError('missing GradidoID for user.', { id: this.user.id })
|
||||
}
|
||||
// example gradido id: 03857ac1-9cc2-483e-8a91-e5b10f5b8d16 =>
|
||||
// wholeHex: '03857ac19cc2483e8a91e5b10f5b8d16']
|
||||
const wholeHex = uuid4ToBuffer(this.user.gradidoID)
|
||||
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]
|
||||
const keyPair = parentKeys.derive(parts)
|
||||
if (this.user.derive1Pubkey && this.user.derive1Pubkey.compare(keyPair.publicKey) !== 0) {
|
||||
throw new LogError(
|
||||
'The freshly derived public key does not correspond to the stored public key',
|
||||
)
|
||||
}
|
||||
if (!this.user.derive1Pubkey) {
|
||||
this.user.derive1Pubkey = keyPair.publicKey
|
||||
}
|
||||
return keyPair
|
||||
}
|
||||
}
|
||||
@ -1,24 +0,0 @@
|
||||
import { Account } from '@entity/Account'
|
||||
import { User } from '@entity/User'
|
||||
|
||||
import { UserIdentifier } from '@/graphql/input/UserIdentifier'
|
||||
import { getDataSource } from '@/typeorm/DataSource'
|
||||
|
||||
export const UserRepository = getDataSource()
|
||||
.getRepository(User)
|
||||
.extend({
|
||||
async findAccountByUserIdentifier({
|
||||
uuid,
|
||||
accountNr,
|
||||
}: UserIdentifier): Promise<Account | undefined> {
|
||||
const user = await this.findOne({
|
||||
where: { gradidoID: uuid, accounts: { derivationIndex: accountNr ?? 1 } },
|
||||
relations: { accounts: true },
|
||||
})
|
||||
if (user && user.accounts?.length === 1) {
|
||||
const account = user.accounts[0]
|
||||
account.user = user
|
||||
return account
|
||||
}
|
||||
},
|
||||
})
|
||||
@ -1 +0,0 @@
|
||||
export const TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY = 'transmitToIota'
|
||||
@ -1,35 +0,0 @@
|
||||
import { Community } from '@entity/Community'
|
||||
import { Transaction } from '@entity/Transaction'
|
||||
import { Field, Message } from 'protobufjs'
|
||||
|
||||
import { AbstractTransaction } from '../AbstractTransaction'
|
||||
|
||||
// https://www.npmjs.com/package/@apollo/protobufjs
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
export class CommunityRoot extends Message<CommunityRoot> implements AbstractTransaction {
|
||||
public constructor(community?: Community) {
|
||||
if (community) {
|
||||
super({
|
||||
rootPubkey: community.rootPubkey,
|
||||
gmwPubkey: community.gmwAccount?.derive2Pubkey,
|
||||
aufPubkey: community.aufAccount?.derive2Pubkey,
|
||||
})
|
||||
} else {
|
||||
super()
|
||||
}
|
||||
}
|
||||
|
||||
@Field.d(1, 'bytes')
|
||||
public rootPubkey: Buffer
|
||||
|
||||
// community public budget account
|
||||
@Field.d(2, 'bytes')
|
||||
public gmwPubkey: Buffer
|
||||
|
||||
// community compensation and environment founds account
|
||||
@Field.d(3, 'bytes')
|
||||
public aufPubkey: Buffer
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function, @typescript-eslint/no-unused-vars
|
||||
public fillTransactionRecipe(recipe: Transaction): void {}
|
||||
}
|
||||
@ -1,41 +0,0 @@
|
||||
import { Field, Message } from 'protobufjs'
|
||||
|
||||
import { base64ToBuffer } from '@/utils/typeConverter'
|
||||
|
||||
import { GradidoTransaction } from './GradidoTransaction'
|
||||
import { TimestampSeconds } from './TimestampSeconds'
|
||||
|
||||
/*
|
||||
id will be set by Node server
|
||||
running_hash will be also set by Node server,
|
||||
calculated from previous transaction running_hash and this id, transaction and received
|
||||
*/
|
||||
|
||||
// https://www.npmjs.com/package/@apollo/protobufjs
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
export class ConfirmedTransaction extends Message<ConfirmedTransaction> {
|
||||
static fromBase64(base64: string): ConfirmedTransaction {
|
||||
return ConfirmedTransaction.decode(new Uint8Array(base64ToBuffer(base64)))
|
||||
}
|
||||
|
||||
@Field.d(1, 'uint64')
|
||||
id: Long
|
||||
|
||||
@Field.d(2, 'GradidoTransaction')
|
||||
transaction: GradidoTransaction
|
||||
|
||||
@Field.d(3, 'TimestampSeconds')
|
||||
confirmedAt: TimestampSeconds
|
||||
|
||||
@Field.d(4, 'string')
|
||||
versionNumber: string
|
||||
|
||||
@Field.d(5, 'bytes')
|
||||
runningHash: Buffer
|
||||
|
||||
@Field.d(6, 'bytes')
|
||||
messageId: Buffer
|
||||
|
||||
@Field.d(7, 'string')
|
||||
accountBalance: string
|
||||
}
|
||||
@ -1,22 +0,0 @@
|
||||
import 'reflect-metadata'
|
||||
import { TransactionErrorType } from '@enum/TransactionErrorType'
|
||||
|
||||
import { TransactionDraft } from '@/graphql/input/TransactionDraft'
|
||||
import { TransactionError } from '@/graphql/model/TransactionError'
|
||||
|
||||
import { GradidoCreation } from './GradidoCreation'
|
||||
|
||||
describe('proto/3.3/GradidoCreation', () => {
|
||||
it('test with missing targetDate', () => {
|
||||
const transactionDraft = new TransactionDraft()
|
||||
expect(() => {
|
||||
// eslint-disable-next-line no-new
|
||||
new GradidoCreation(transactionDraft)
|
||||
}).toThrowError(
|
||||
new TransactionError(
|
||||
TransactionErrorType.MISSING_PARAMETER,
|
||||
'missing targetDate for contribution',
|
||||
),
|
||||
)
|
||||
})
|
||||
})
|
||||
@ -1,53 +0,0 @@
|
||||
import { Account } from '@entity/Account'
|
||||
import { Transaction } from '@entity/Transaction'
|
||||
import { Decimal } from 'decimal.js-light'
|
||||
import { Field, Message } from 'protobufjs'
|
||||
|
||||
import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType'
|
||||
import { TransactionDraft } from '@/graphql/input/TransactionDraft'
|
||||
import { TransactionError } from '@/graphql/model/TransactionError'
|
||||
|
||||
import { AbstractTransaction } from '../AbstractTransaction'
|
||||
|
||||
import { TimestampSeconds } from './TimestampSeconds'
|
||||
import { TransferAmount } from './TransferAmount'
|
||||
|
||||
// need signature from group admin or
|
||||
// percent of group users another than the receiver
|
||||
// https://www.npmjs.com/package/@apollo/protobufjs
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
export class GradidoCreation extends Message<GradidoCreation> implements AbstractTransaction {
|
||||
constructor(transaction?: TransactionDraft, recipientAccount?: Account) {
|
||||
if (transaction) {
|
||||
if (!transaction.targetDate) {
|
||||
throw new TransactionError(
|
||||
TransactionErrorType.MISSING_PARAMETER,
|
||||
'missing targetDate for contribution',
|
||||
)
|
||||
}
|
||||
super({
|
||||
recipient: new TransferAmount({
|
||||
amount: transaction.amount.toString(),
|
||||
pubkey: recipientAccount?.derive2Pubkey,
|
||||
}),
|
||||
targetDate: new TimestampSeconds(new Date(transaction.targetDate)),
|
||||
})
|
||||
} else {
|
||||
super()
|
||||
}
|
||||
}
|
||||
|
||||
// recipient: TransferAmount contain
|
||||
// - recipient public key
|
||||
// - amount
|
||||
// - communityId // only set if not the same as recipient community
|
||||
@Field.d(1, TransferAmount)
|
||||
public recipient: TransferAmount
|
||||
|
||||
@Field.d(3, 'TimestampSeconds')
|
||||
public targetDate: TimestampSeconds
|
||||
|
||||
public fillTransactionRecipe(recipe: Transaction): void {
|
||||
recipe.amount = new Decimal(this.recipient.amount ?? 0)
|
||||
}
|
||||
}
|
||||
@ -1,42 +0,0 @@
|
||||
import { Transaction } from '@entity/Transaction'
|
||||
import Decimal from 'decimal.js-light'
|
||||
import { Field, Message } from 'protobufjs'
|
||||
|
||||
import { AbstractTransaction } from '../AbstractTransaction'
|
||||
|
||||
import { GradidoTransfer } from './GradidoTransfer'
|
||||
import { TimestampSeconds } from './TimestampSeconds'
|
||||
|
||||
// transaction type for chargeable transactions
|
||||
// for transaction for people which haven't a account already
|
||||
// consider using a seed number for key pair generation for recipient
|
||||
// using seed as redeem key for claiming transaction, technically make a default Transfer transaction from recipient address
|
||||
// seed must be long enough to prevent brute force, maybe base64 encoded
|
||||
// to own account
|
||||
// https://www.npmjs.com/package/@apollo/protobufjs
|
||||
export class GradidoDeferredTransfer
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
extends Message<GradidoDeferredTransfer>
|
||||
implements AbstractTransaction
|
||||
{
|
||||
// amount is amount with decay for time span between transaction was received and timeout
|
||||
// useable amount can be calculated
|
||||
// recipient address don't need to be registered in blockchain with register address
|
||||
@Field.d(1, GradidoTransfer)
|
||||
public transfer: GradidoTransfer
|
||||
|
||||
// if timeout timestamp is reached if it wasn't used, it will be booked back minus decay
|
||||
// technically on blockchain no additional transaction will be created because how should sign it?
|
||||
// the decay for amount and the seconds until timeout is lost no matter what happened
|
||||
// consider is as fee for this service
|
||||
// rest decay could be transferred back as separate transaction
|
||||
@Field.d(2, 'TimestampSeconds')
|
||||
public timeout: TimestampSeconds
|
||||
|
||||
// split for n recipient
|
||||
// max gradido per recipient? or per transaction with cool down?
|
||||
|
||||
public fillTransactionRecipe(recipe: Transaction): void {
|
||||
recipe.amount = new Decimal(this.transfer.sender.amount ?? 0)
|
||||
}
|
||||
}
|
||||
@ -1,48 +0,0 @@
|
||||
import { Field, Message } from 'protobufjs'
|
||||
|
||||
import { LogError } from '@/server/LogError'
|
||||
|
||||
import { SignatureMap } from './SignatureMap'
|
||||
import { SignaturePair } from './SignaturePair'
|
||||
import { TransactionBody } from './TransactionBody'
|
||||
|
||||
// https://www.npmjs.com/package/@apollo/protobufjs
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
export class GradidoTransaction extends Message<GradidoTransaction> {
|
||||
constructor(body?: TransactionBody) {
|
||||
if (body) {
|
||||
super({
|
||||
sigMap: new SignatureMap(),
|
||||
bodyBytes: Buffer.from(TransactionBody.encode(body).finish()),
|
||||
})
|
||||
} else {
|
||||
super()
|
||||
}
|
||||
}
|
||||
|
||||
@Field.d(1, SignatureMap)
|
||||
public sigMap: SignatureMap
|
||||
|
||||
// inspired by Hedera
|
||||
// bodyBytes are the payload for signature
|
||||
// bodyBytes are serialized TransactionBody
|
||||
@Field.d(2, 'bytes')
|
||||
public bodyBytes: Buffer
|
||||
|
||||
// if it is a cross group transaction the parent message
|
||||
// id from outbound transaction or other by cross
|
||||
@Field.d(3, 'bytes')
|
||||
public parentMessageId?: Buffer
|
||||
|
||||
getFirstSignature(): SignaturePair {
|
||||
const sigPair = this.sigMap.sigPair
|
||||
if (sigPair.length !== 1) {
|
||||
throw new LogError("signature count don't like expected")
|
||||
}
|
||||
return sigPair[0]
|
||||
}
|
||||
|
||||
getTransactionBody(): TransactionBody {
|
||||
return TransactionBody.fromBodyBytes(this.bodyBytes)
|
||||
}
|
||||
}
|
||||
@ -1,49 +0,0 @@
|
||||
import { Account } from '@entity/Account'
|
||||
import { Transaction } from '@entity/Transaction'
|
||||
import Decimal from 'decimal.js-light'
|
||||
import { Field, Message } from 'protobufjs'
|
||||
|
||||
import { TransactionDraft } from '@/graphql/input/TransactionDraft'
|
||||
|
||||
import { AbstractTransaction } from '../AbstractTransaction'
|
||||
|
||||
import { TransferAmount } from './TransferAmount'
|
||||
|
||||
// https://www.npmjs.com/package/@apollo/protobufjs
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
export class GradidoTransfer extends Message<GradidoTransfer> implements AbstractTransaction {
|
||||
constructor(
|
||||
transaction?: TransactionDraft,
|
||||
signingAccount?: Account,
|
||||
recipientAccount?: Account,
|
||||
coinOrigin?: string,
|
||||
) {
|
||||
if (transaction) {
|
||||
super({
|
||||
sender: new TransferAmount({
|
||||
amount: transaction.amount.toString(),
|
||||
pubkey: signingAccount?.derive2Pubkey,
|
||||
communityId: coinOrigin,
|
||||
}),
|
||||
recipient: recipientAccount?.derive2Pubkey,
|
||||
})
|
||||
} else {
|
||||
super()
|
||||
}
|
||||
}
|
||||
|
||||
// sender: TransferAmount contain
|
||||
// - sender public key
|
||||
// - amount
|
||||
// - communityId // only set if not the same as sender and recipient community
|
||||
@Field.d(1, TransferAmount)
|
||||
public sender: TransferAmount
|
||||
|
||||
// the recipient public key
|
||||
@Field.d(2, 'bytes')
|
||||
public recipient: Buffer
|
||||
|
||||
public fillTransactionRecipe(recipe: Transaction): void {
|
||||
recipe.amount = new Decimal(this.sender?.amount ?? 0)
|
||||
}
|
||||
}
|
||||
@ -1,23 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import { Transaction } from '@entity/Transaction'
|
||||
import { Field, Message } from 'protobufjs'
|
||||
|
||||
import { AbstractTransaction } from '../AbstractTransaction'
|
||||
|
||||
// connect group together
|
||||
// only CrossGroupType CROSS (in TransactionBody)
|
||||
// https://www.npmjs.com/package/@apollo/protobufjs
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
export class GroupFriendsUpdate extends Message<GroupFriendsUpdate> implements AbstractTransaction {
|
||||
// if set to true, colors of this both groups are trait as the same
|
||||
// on creation user get coins still in there color
|
||||
// on transfer into another group which a connection exist,
|
||||
// coins will be automatic swapped into user group color coin
|
||||
// (if fusion between src coin and dst coin is enabled)
|
||||
@Field.d(1, 'bool')
|
||||
public colorFusion: boolean
|
||||
|
||||
public fillTransactionRecipe(recipe: Transaction): void {
|
||||
throw new Error('Method not implemented.')
|
||||
}
|
||||
}
|
||||
@ -1,29 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-empty-function */
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import { Transaction } from '@entity/Transaction'
|
||||
import { Field, Message } from 'protobufjs'
|
||||
|
||||
import { AddressType } from '@/data/proto/3_3/enum/AddressType'
|
||||
|
||||
import { AbstractTransaction } from '../AbstractTransaction'
|
||||
|
||||
// https://www.npmjs.com/package/@apollo/protobufjs
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
export class RegisterAddress extends Message<RegisterAddress> implements AbstractTransaction {
|
||||
@Field.d(1, 'bytes')
|
||||
public userPubkey: Buffer
|
||||
|
||||
@Field.d(2, AddressType)
|
||||
public addressType: AddressType
|
||||
|
||||
@Field.d(3, 'bytes')
|
||||
public nameHash: Buffer
|
||||
|
||||
@Field.d(4, 'bytes')
|
||||
public accountPubkey: Buffer
|
||||
|
||||
@Field.d(5, 'uint32')
|
||||
public derivationIndex?: number
|
||||
|
||||
public fillTransactionRecipe(_recipe: Transaction): void {}
|
||||
}
|
||||
@ -1,14 +0,0 @@
|
||||
import { Field, Message } from 'protobufjs'
|
||||
|
||||
import { SignaturePair } from './SignaturePair'
|
||||
|
||||
// https://www.npmjs.com/package/@apollo/protobufjs
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
export class SignatureMap extends Message<SignatureMap> {
|
||||
constructor() {
|
||||
super({ sigPair: [] })
|
||||
}
|
||||
|
||||
@Field.d(1, SignaturePair, 'repeated')
|
||||
public sigPair: SignaturePair[]
|
||||
}
|
||||
@ -1,15 +0,0 @@
|
||||
import { Field, Message } from 'protobufjs'
|
||||
|
||||
// https://www.npmjs.com/package/@apollo/protobufjs
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
export class SignaturePair extends Message<SignaturePair> {
|
||||
@Field.d(1, 'bytes')
|
||||
public pubKey: Buffer
|
||||
|
||||
@Field.d(2, 'bytes')
|
||||
public signature: Buffer
|
||||
|
||||
public validate(): boolean {
|
||||
return this.pubKey.length === 32 && this.signature.length === 64
|
||||
}
|
||||
}
|
||||
@ -1,16 +0,0 @@
|
||||
import { Timestamp } from './Timestamp'
|
||||
|
||||
describe('test timestamp constructor', () => {
|
||||
it('with date input object', () => {
|
||||
const now = new Date('2011-04-17T12:01:10.109')
|
||||
const timestamp = new Timestamp(now)
|
||||
expect(timestamp.seconds).toEqual(1303041670)
|
||||
expect(timestamp.nanoSeconds).toEqual(109000000)
|
||||
})
|
||||
|
||||
it('with milliseconds number input', () => {
|
||||
const timestamp = new Timestamp(1303041670109)
|
||||
expect(timestamp.seconds).toEqual(1303041670)
|
||||
expect(timestamp.nanoSeconds).toEqual(109000000)
|
||||
})
|
||||
})
|
||||
@ -1,27 +0,0 @@
|
||||
import { Field, Message } from 'protobufjs'
|
||||
|
||||
// https://www.npmjs.com/package/@apollo/protobufjs
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
export class Timestamp extends Message<Timestamp> {
|
||||
public constructor(input?: Date | number) {
|
||||
let seconds = 0
|
||||
let nanoSeconds = 0
|
||||
if (input instanceof Date) {
|
||||
seconds = Math.floor(input.getTime() / 1000)
|
||||
nanoSeconds = (input.getTime() % 1000) * 1000000 // Convert milliseconds to nanoseconds
|
||||
} else if (typeof input === 'number') {
|
||||
// Calculate seconds and nanoseconds from milliseconds
|
||||
seconds = Math.floor(input / 1000)
|
||||
nanoSeconds = (input % 1000) * 1000000
|
||||
}
|
||||
super({ seconds, nanoSeconds })
|
||||
}
|
||||
|
||||
// Number of complete seconds since the start of the epoch
|
||||
@Field.d(1, 'int64')
|
||||
public seconds: number
|
||||
|
||||
// Number of nanoseconds since the start of the last second
|
||||
@Field.d(2, 'int32')
|
||||
public nanoSeconds: number
|
||||
}
|
||||
@ -1,14 +0,0 @@
|
||||
import { TimestampSeconds } from './TimestampSeconds'
|
||||
|
||||
describe('test TimestampSeconds constructor', () => {
|
||||
it('with date input object', () => {
|
||||
const now = new Date('2011-04-17T12:01:10.109')
|
||||
const timestamp = new TimestampSeconds(now)
|
||||
expect(timestamp.seconds).toEqual(1303041670)
|
||||
})
|
||||
|
||||
it('with milliseconds number input', () => {
|
||||
const timestamp = new TimestampSeconds(1303041670109)
|
||||
expect(timestamp.seconds).toEqual(1303041670)
|
||||
})
|
||||
})
|
||||
@ -1,20 +0,0 @@
|
||||
import { Field, Message } from 'protobufjs'
|
||||
|
||||
// https://www.npmjs.com/package/@apollo/protobufjs
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
export class TimestampSeconds extends Message<TimestampSeconds> {
|
||||
public constructor(input?: Date | number) {
|
||||
let seconds = 0
|
||||
// Calculate seconds from milliseconds
|
||||
if (input instanceof Date) {
|
||||
seconds = Math.floor(input.getTime() / 1000)
|
||||
} else if (typeof input === 'number') {
|
||||
seconds = Math.floor(input / 1000)
|
||||
}
|
||||
super({ seconds })
|
||||
}
|
||||
|
||||
// Number of complete seconds since the start of the epoch
|
||||
@Field.d(1, 'int64')
|
||||
public seconds: number
|
||||
}
|
||||
@ -1,148 +0,0 @@
|
||||
import { Transaction } from '@entity/Transaction'
|
||||
import { Field, Message, OneOf } from 'protobufjs'
|
||||
|
||||
import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType'
|
||||
import { CommunityDraft } from '@/graphql/input/CommunityDraft'
|
||||
import { TransactionDraft } from '@/graphql/input/TransactionDraft'
|
||||
import { TransactionError } from '@/graphql/model/TransactionError'
|
||||
import { logger } from '@/logging/logger'
|
||||
import { LogError } from '@/server/LogError'
|
||||
import { timestampToDate } from '@/utils/typeConverter'
|
||||
|
||||
import { AbstractTransaction } from '../AbstractTransaction'
|
||||
|
||||
import { CommunityRoot } from './CommunityRoot'
|
||||
import { PROTO_TRANSACTION_BODY_VERSION_NUMBER } from './const'
|
||||
import { CrossGroupType } from './enum/CrossGroupType'
|
||||
import { TransactionType } from './enum/TransactionType'
|
||||
import { GradidoCreation } from './GradidoCreation'
|
||||
import { GradidoDeferredTransfer } from './GradidoDeferredTransfer'
|
||||
import { GradidoTransfer } from './GradidoTransfer'
|
||||
import { GroupFriendsUpdate } from './GroupFriendsUpdate'
|
||||
import { RegisterAddress } from './RegisterAddress'
|
||||
import { Timestamp } from './Timestamp'
|
||||
|
||||
// https://www.npmjs.com/package/@apollo/protobufjs
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
export class TransactionBody extends Message<TransactionBody> {
|
||||
public constructor(transaction?: TransactionDraft | CommunityDraft) {
|
||||
if (transaction) {
|
||||
super({
|
||||
memo: 'Not implemented yet',
|
||||
createdAt: new Timestamp(new Date(transaction.createdAt)),
|
||||
versionNumber: PROTO_TRANSACTION_BODY_VERSION_NUMBER,
|
||||
type: CrossGroupType.LOCAL,
|
||||
otherGroup: '',
|
||||
})
|
||||
} else {
|
||||
super()
|
||||
}
|
||||
}
|
||||
|
||||
public static fromBodyBytes(bodyBytes: Buffer) {
|
||||
try {
|
||||
return TransactionBody.decode(new Uint8Array(bodyBytes))
|
||||
} catch (error) {
|
||||
logger.error('error decoding body from gradido transaction: %s', error)
|
||||
throw new TransactionError(
|
||||
TransactionErrorType.PROTO_DECODE_ERROR,
|
||||
'cannot decode body from gradido transaction',
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Field.d(1, 'string')
|
||||
public memo: string
|
||||
|
||||
@Field.d(2, Timestamp)
|
||||
public createdAt: Timestamp
|
||||
|
||||
@Field.d(3, 'string')
|
||||
public versionNumber: string
|
||||
|
||||
@Field.d(4, CrossGroupType)
|
||||
public type: CrossGroupType
|
||||
|
||||
@Field.d(5, 'string')
|
||||
public otherGroup: string
|
||||
|
||||
@OneOf.d(
|
||||
'gradidoTransfer',
|
||||
'gradidoCreation',
|
||||
'groupFriendsUpdate',
|
||||
'registerAddress',
|
||||
'gradidoDeferredTransfer',
|
||||
'communityRoot',
|
||||
)
|
||||
public data: string
|
||||
|
||||
@Field.d(6, 'GradidoTransfer')
|
||||
transfer?: GradidoTransfer
|
||||
|
||||
@Field.d(7, 'GradidoCreation')
|
||||
creation?: GradidoCreation
|
||||
|
||||
@Field.d(8, 'GroupFriendsUpdate')
|
||||
groupFriendsUpdate?: GroupFriendsUpdate
|
||||
|
||||
@Field.d(9, 'RegisterAddress')
|
||||
registerAddress?: RegisterAddress
|
||||
|
||||
@Field.d(10, 'GradidoDeferredTransfer')
|
||||
deferredTransfer?: GradidoDeferredTransfer
|
||||
|
||||
@Field.d(11, 'CommunityRoot')
|
||||
communityRoot?: CommunityRoot
|
||||
|
||||
public getTransactionType(): TransactionType | undefined {
|
||||
if (this.transfer) return TransactionType.GRADIDO_TRANSFER
|
||||
else if (this.creation) return TransactionType.GRADIDO_CREATION
|
||||
else if (this.groupFriendsUpdate) return TransactionType.GROUP_FRIENDS_UPDATE
|
||||
else if (this.registerAddress) return TransactionType.REGISTER_ADDRESS
|
||||
else if (this.deferredTransfer) return TransactionType.GRADIDO_DEFERRED_TRANSFER
|
||||
else if (this.communityRoot) return TransactionType.COMMUNITY_ROOT
|
||||
}
|
||||
|
||||
// The `TransactionBody` class utilizes Protobuf's `OneOf` field structure which, according to Protobuf documentation
|
||||
// (https://protobuf.dev/programming-guides/proto3/#oneof), allows only one field within the group to be set at a time.
|
||||
// Therefore, accessing the `getTransactionDetails()` method returns the first initialized value among the defined fields,
|
||||
// each of which should be of type AbstractTransaction. It's important to note that due to the nature of Protobuf's `OneOf`,
|
||||
// only one type from the defined options can be set within the object obtained from Protobuf.
|
||||
//
|
||||
// If multiple fields are set in a single object, the method `getTransactionDetails()` will return the first defined value
|
||||
// based on the order of checks. Developers should handle this behavior according to the expected Protobuf structure.
|
||||
public getTransactionDetails(): AbstractTransaction | undefined {
|
||||
if (this.transfer) return this.transfer
|
||||
if (this.creation) return this.creation
|
||||
if (this.groupFriendsUpdate) return this.groupFriendsUpdate
|
||||
if (this.registerAddress) return this.registerAddress
|
||||
if (this.deferredTransfer) return this.deferredTransfer
|
||||
if (this.communityRoot) return this.communityRoot
|
||||
}
|
||||
|
||||
public fillTransactionRecipe(recipe: Transaction): void {
|
||||
recipe.createdAt = timestampToDate(this.createdAt)
|
||||
recipe.protocolVersion = this.versionNumber
|
||||
const transactionType = this.getTransactionType()
|
||||
if (!transactionType) {
|
||||
throw new LogError("invalid TransactionBody couldn't determine transaction type")
|
||||
}
|
||||
recipe.type = transactionType.valueOf()
|
||||
this.getTransactionDetails()?.fillTransactionRecipe(recipe)
|
||||
}
|
||||
|
||||
public getRecipientPublicKey(): Buffer | undefined {
|
||||
if (this.transfer) {
|
||||
// this.transfer.recipient contains the publicKey of the recipient
|
||||
return this.transfer.recipient
|
||||
}
|
||||
if (this.creation) {
|
||||
return this.creation.recipient.pubkey
|
||||
}
|
||||
if (this.deferredTransfer) {
|
||||
// this.deferredTransfer.transfer.recipient contains the publicKey of the recipient
|
||||
return this.deferredTransfer.transfer.recipient
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
@ -1,16 +0,0 @@
|
||||
import { Field, Message } from 'protobufjs'
|
||||
|
||||
// https://www.npmjs.com/package/@apollo/protobufjs
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
export class TransferAmount extends Message<TransferAmount> {
|
||||
@Field.d(1, 'bytes')
|
||||
public pubkey: Buffer
|
||||
|
||||
@Field.d(2, 'string')
|
||||
public amount: string
|
||||
|
||||
// community which created this coin
|
||||
// used for colored coins
|
||||
@Field.d(3, 'string')
|
||||
public communityId: string
|
||||
}
|
||||
@ -1 +0,0 @@
|
||||
export const PROTO_TRANSACTION_BODY_VERSION_NUMBER = '3.3'
|
||||
@ -1,14 +0,0 @@
|
||||
/**
|
||||
* Enum for protobuf
|
||||
* used from RegisterAddress to determine account type
|
||||
* master implementation: https://github.com/gradido/gradido_protocol/blob/master/proto/gradido/register_address.proto
|
||||
*/
|
||||
export enum AddressType {
|
||||
NONE = 0, // if no address was found
|
||||
COMMUNITY_HUMAN = 1, // creation account for human
|
||||
COMMUNITY_GMW = 2, // community public budget account
|
||||
COMMUNITY_AUF = 3, // community compensation and environment founds account
|
||||
COMMUNITY_PROJECT = 4, // no creations allowed
|
||||
SUBACCOUNT = 5, // no creations allowed
|
||||
CRYPTO_ACCOUNT = 6, // user control his keys, no creations
|
||||
}
|
||||
@ -1,22 +0,0 @@
|
||||
/**
|
||||
* Enum for protobuf
|
||||
* Determine Cross Group type of Transactions
|
||||
* LOCAL: no cross group transactions, sender and recipient community are the same, only one transaction
|
||||
* INBOUND: cross group transaction, Inbound part. On recipient community chain. Recipient side by Transfer Transactions
|
||||
* OUTBOUND: cross group transaction, Outbound part. On sender community chain. Sender side by Transfer Transactions
|
||||
* CROSS: for cross group transaction which haven't a direction like group friend update
|
||||
* master implementation: https://github.com/gradido/gradido_protocol/blob/master/proto/gradido/transaction_body.proto
|
||||
*
|
||||
* Transaction Handling differ from database focused backend
|
||||
* In Backend for each transfer transaction there are always two entries in db,
|
||||
* on for sender user and one for recipient user despite storing basically the same data two times
|
||||
* In Blockchain Implementation there only two transactions on cross group transactions, one for
|
||||
* the sender community chain, one for the recipient community chain
|
||||
* if the transaction stay in the community there is only one transaction
|
||||
*/
|
||||
export enum CrossGroupType {
|
||||
LOCAL = 0,
|
||||
INBOUND = 1,
|
||||
OUTBOUND = 2,
|
||||
CROSS = 3,
|
||||
}
|
||||
@ -1,13 +0,0 @@
|
||||
/**
|
||||
* based on TransactionBody data oneOf
|
||||
* https://github.com/gradido/gradido_protocol/blob/master/proto/gradido/transaction_body.proto
|
||||
* for storing type in db as number
|
||||
*/
|
||||
export enum TransactionType {
|
||||
GRADIDO_TRANSFER = 1,
|
||||
GRADIDO_CREATION = 2,
|
||||
GROUP_FRIENDS_UPDATE = 3,
|
||||
REGISTER_ADDRESS = 4,
|
||||
GRADIDO_DEFERRED_TRANSFER = 5,
|
||||
COMMUNITY_ROOT = 6,
|
||||
}
|
||||
@ -1,5 +0,0 @@
|
||||
import { Transaction } from '@entity/Transaction'
|
||||
|
||||
export abstract class AbstractTransaction {
|
||||
public abstract fillTransactionRecipe(recipe: Transaction): void
|
||||
}
|
||||
@ -1,138 +0,0 @@
|
||||
import { Account } from '@entity/Account'
|
||||
import { Community } from '@entity/Community'
|
||||
|
||||
import { InputTransactionType } from '@/graphql/enum/InputTransactionType'
|
||||
import { CommunityDraft } from '@/graphql/input/CommunityDraft'
|
||||
import { TransactionDraft } from '@/graphql/input/TransactionDraft'
|
||||
import { LogError } from '@/server/LogError'
|
||||
|
||||
import { CommunityRoot } from './3_3/CommunityRoot'
|
||||
import { CrossGroupType } from './3_3/enum/CrossGroupType'
|
||||
import { GradidoCreation } from './3_3/GradidoCreation'
|
||||
import { GradidoTransfer } from './3_3/GradidoTransfer'
|
||||
import { TransactionBody } from './3_3/TransactionBody'
|
||||
|
||||
export class TransactionBodyBuilder {
|
||||
private signingAccount?: Account
|
||||
private recipientAccount?: Account
|
||||
private body: TransactionBody | undefined
|
||||
|
||||
// https://refactoring.guru/design-patterns/builder/typescript/example
|
||||
/**
|
||||
* A fresh builder instance should contain a blank product object, which is
|
||||
* used in further assembly.
|
||||
*/
|
||||
constructor() {
|
||||
this.reset()
|
||||
}
|
||||
|
||||
public reset(): void {
|
||||
this.body = undefined
|
||||
this.signingAccount = undefined
|
||||
this.recipientAccount = undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Concrete Builders are supposed to provide their own methods for
|
||||
* retrieving results. That's because various types of builders may create
|
||||
* entirely different products that don't follow the same interface.
|
||||
* Therefore, such methods cannot be declared in the base Builder interface
|
||||
* (at least in a statically typed programming language).
|
||||
*
|
||||
* Usually, after returning the end result to the client, a builder instance
|
||||
* is expected to be ready to start producing another product. That's why
|
||||
* it's a usual practice to call the reset method at the end of the
|
||||
* `getProduct` method body. However, this behavior is not mandatory, and
|
||||
* you can make your builders wait for an explicit reset call from the
|
||||
* client code before disposing of the previous result.
|
||||
*/
|
||||
public build(): TransactionBody {
|
||||
const result = this.getTransactionBody()
|
||||
this.reset()
|
||||
return result
|
||||
}
|
||||
|
||||
public getTransactionBody(): TransactionBody {
|
||||
if (!this.body) {
|
||||
throw new LogError(
|
||||
'cannot build Transaction Body, missing information, please call at least fromTransactionDraft or fromCommunityDraft',
|
||||
)
|
||||
}
|
||||
return this.body
|
||||
}
|
||||
|
||||
public getSigningAccount(): Account | undefined {
|
||||
return this.signingAccount
|
||||
}
|
||||
|
||||
public getRecipientAccount(): Account | undefined {
|
||||
return this.recipientAccount
|
||||
}
|
||||
|
||||
public setSigningAccount(signingAccount: Account): TransactionBodyBuilder {
|
||||
this.signingAccount = signingAccount
|
||||
return this
|
||||
}
|
||||
|
||||
public setRecipientAccount(recipientAccount: Account): TransactionBodyBuilder {
|
||||
this.recipientAccount = recipientAccount
|
||||
return this
|
||||
}
|
||||
|
||||
public setCrossGroupType(type: CrossGroupType): this {
|
||||
if (!this.body) {
|
||||
throw new LogError(
|
||||
'body is undefined, please call fromTransactionDraft or fromCommunityDraft before',
|
||||
)
|
||||
}
|
||||
this.body.type = type
|
||||
return this
|
||||
}
|
||||
|
||||
public setOtherGroup(otherGroup: string): this {
|
||||
if (!this.body) {
|
||||
throw new LogError(
|
||||
'body is undefined, please call fromTransactionDraft or fromCommunityDraft before',
|
||||
)
|
||||
}
|
||||
this.body.otherGroup = otherGroup
|
||||
return this
|
||||
}
|
||||
|
||||
public fromTransactionDraft(transactionDraft: TransactionDraft): TransactionBodyBuilder {
|
||||
this.body = new TransactionBody(transactionDraft)
|
||||
// TODO: load pubkeys for sender and recipient user from db
|
||||
switch (transactionDraft.type) {
|
||||
case InputTransactionType.CREATION:
|
||||
if (!this.recipientAccount) {
|
||||
throw new LogError('missing recipient account for creation transaction!')
|
||||
}
|
||||
this.body.creation = new GradidoCreation(transactionDraft, this.recipientAccount)
|
||||
this.body.data = 'gradidoCreation'
|
||||
break
|
||||
case InputTransactionType.SEND:
|
||||
case InputTransactionType.RECEIVE:
|
||||
if (!this.recipientAccount || !this.signingAccount) {
|
||||
throw new LogError('missing signing and/or recipient account for transfer transaction!')
|
||||
}
|
||||
this.body.transfer = new GradidoTransfer(
|
||||
transactionDraft,
|
||||
this.signingAccount,
|
||||
this.recipientAccount,
|
||||
)
|
||||
this.body.data = 'gradidoTransfer'
|
||||
break
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
public fromCommunityDraft(
|
||||
communityDraft: CommunityDraft,
|
||||
community: Community,
|
||||
): TransactionBodyBuilder {
|
||||
this.body = new TransactionBody(communityDraft)
|
||||
this.body.communityRoot = new CommunityRoot(community)
|
||||
this.body.data = 'communityRoot'
|
||||
return this
|
||||
}
|
||||
}
|
||||
2
dlt-connector/src/graphql/const.ts
Normal file
2
dlt-connector/src/graphql/const.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export const MEMO_MAX_CHARS = 255
|
||||
export const MEMO_MIN_CHARS = 5
|
||||
@ -3,9 +3,13 @@ import { registerEnumType } from 'type-graphql'
|
||||
// enum for graphql but with int because it is the same in backend
|
||||
// for transaction type from backend
|
||||
export enum InputTransactionType {
|
||||
CREATION = 1,
|
||||
SEND = 2,
|
||||
RECEIVE = 3,
|
||||
GRADIDO_TRANSFER = 'GRADIDO_TRANSFER',
|
||||
GRADIDO_CREATION = 'GRADIDO_CREATION',
|
||||
GROUP_FRIENDS_UPDATE = 'GROUP_FRIENDS_UPDATE',
|
||||
REGISTER_ADDRESS = 'REGISTER_ADDRESS',
|
||||
GRADIDO_DEFERRED_TRANSFER = 'GRADIDO_DEFERRED_TRANSFER',
|
||||
GRADIDO_REDEEM_DEFERRED_TRANSFER = 'GRADIDO_REDEEM_DEFERRED_TRANSFER',
|
||||
COMMUNITY_ROOT = 'COMMUNITY_ROOT',
|
||||
}
|
||||
|
||||
registerEnumType(InputTransactionType, {
|
||||
|
||||
@ -5,6 +5,7 @@ import { registerEnumType } from 'type-graphql'
|
||||
export enum TransactionErrorType {
|
||||
NOT_IMPLEMENTED_YET = 'Not Implemented yet',
|
||||
MISSING_PARAMETER = 'Missing parameter',
|
||||
INVALID_PARAMETER = 'Invalid parameter',
|
||||
ALREADY_EXIST = 'Already exist',
|
||||
DB_ERROR = 'DB Error',
|
||||
PROTO_DECODE_ERROR = 'Proto Decode Error',
|
||||
@ -12,6 +13,7 @@ export enum TransactionErrorType {
|
||||
INVALID_SIGNATURE = 'Invalid Signature',
|
||||
LOGIC_ERROR = 'Logic Error',
|
||||
NOT_FOUND = 'Not found',
|
||||
VALIDATION_ERROR = 'Validation Error',
|
||||
}
|
||||
|
||||
registerEnumType(TransactionErrorType, {
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
// https://www.npmjs.com/package/@apollo/protobufjs
|
||||
|
||||
import { isValidDateString } from '@validator/DateString'
|
||||
import { IsBoolean, IsUUID } from 'class-validator'
|
||||
import { Field, InputType } from 'type-graphql'
|
||||
|
||||
import { isValidDateString } from '@validator/DateString'
|
||||
|
||||
@InputType()
|
||||
export class CommunityDraft {
|
||||
@Field(() => String)
|
||||
|
||||
15
dlt-connector/src/graphql/input/CommunityUser.ts
Normal file
15
dlt-connector/src/graphql/input/CommunityUser.ts
Normal file
@ -0,0 +1,15 @@
|
||||
// https://www.npmjs.com/package/@apollo/protobufjs
|
||||
|
||||
import { IsPositive, IsUUID } from 'class-validator'
|
||||
import { Field, Int, InputType } from 'type-graphql'
|
||||
|
||||
@InputType()
|
||||
export class CommunityUser {
|
||||
@Field(() => String)
|
||||
@IsUUID('4')
|
||||
uuid: string
|
||||
|
||||
@Field(() => Int, { defaultValue: 1, nullable: true })
|
||||
@IsPositive()
|
||||
accountNr?: number
|
||||
}
|
||||
15
dlt-connector/src/graphql/input/IdentifierSeed.ts
Normal file
15
dlt-connector/src/graphql/input/IdentifierSeed.ts
Normal file
@ -0,0 +1,15 @@
|
||||
// https://www.npmjs.com/package/@apollo/protobufjs
|
||||
|
||||
import { IsString } from 'class-validator'
|
||||
import { Field, InputType } from 'type-graphql'
|
||||
|
||||
@InputType()
|
||||
export class IdentifierSeed {
|
||||
@Field(() => String)
|
||||
@IsString()
|
||||
seed: string
|
||||
|
||||
constructor(seed: string) {
|
||||
this.seed = seed
|
||||
}
|
||||
}
|
||||
@ -1,11 +1,11 @@
|
||||
// https://www.npmjs.com/package/@apollo/protobufjs
|
||||
import { IsEnum, IsObject, IsPositive, ValidateNested } from 'class-validator'
|
||||
import { Decimal } from 'decimal.js-light'
|
||||
import { InputType, Field, Int } from 'type-graphql'
|
||||
|
||||
import { InputTransactionType } from '@enum/InputTransactionType'
|
||||
import { isValidDateString } from '@validator/DateString'
|
||||
import { IsPositiveDecimal } from '@validator/Decimal'
|
||||
import { isValidDateString, isValidNumberString } from '@validator/DateString'
|
||||
import { IsEnum, IsObject, IsPositive, MaxLength, MinLength, ValidateNested } from 'class-validator'
|
||||
import { InputType, Field } from 'type-graphql'
|
||||
|
||||
import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from '@/graphql//const'
|
||||
import { AccountType } from '@/graphql/enum/AccountType'
|
||||
|
||||
import { UserIdentifier } from './UserIdentifier'
|
||||
|
||||
@ -16,18 +16,21 @@ export class TransactionDraft {
|
||||
@ValidateNested()
|
||||
user: UserIdentifier
|
||||
|
||||
@Field(() => UserIdentifier)
|
||||
// not used for simply register address
|
||||
@Field(() => UserIdentifier, { nullable: true })
|
||||
@IsObject()
|
||||
@ValidateNested()
|
||||
linkedUser: UserIdentifier
|
||||
linkedUser?: UserIdentifier
|
||||
|
||||
@Field(() => Int)
|
||||
@IsPositive()
|
||||
backendTransactionId: number
|
||||
// not used for register address
|
||||
@Field(() => String, { nullable: true })
|
||||
@isValidNumberString()
|
||||
amount?: string
|
||||
|
||||
@Field(() => Decimal)
|
||||
@IsPositiveDecimal()
|
||||
amount: Decimal
|
||||
@Field(() => String, { nullable: true })
|
||||
@MaxLength(MEMO_MAX_CHARS)
|
||||
@MinLength(MEMO_MIN_CHARS)
|
||||
memo?: string
|
||||
|
||||
@Field(() => InputTransactionType)
|
||||
@IsEnum(InputTransactionType)
|
||||
@ -41,4 +44,15 @@ export class TransactionDraft {
|
||||
@Field(() => String, { nullable: true })
|
||||
@isValidDateString()
|
||||
targetDate?: string
|
||||
|
||||
// only for deferred transaction
|
||||
// duration in seconds
|
||||
@Field(() => Number, { nullable: true })
|
||||
@IsPositive()
|
||||
timeoutDuration?: number
|
||||
|
||||
// only for register address
|
||||
@Field(() => AccountType, { nullable: true })
|
||||
@IsEnum(AccountType)
|
||||
accountType?: AccountType
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user