mirror of
https://github.com/IT4Change/gradido.git
synced 2026-02-06 09:56:05 +00:00
Merge branch 'master' into 3204-release-1_23_3
This commit is contained in:
commit
6066d403c6
1
.github/workflows/lint_pr.yml
vendored
1
.github/workflows/lint_pr.yml
vendored
@ -27,6 +27,7 @@ jobs:
|
||||
frontend
|
||||
admin
|
||||
database
|
||||
dlt-database
|
||||
release
|
||||
federation
|
||||
dht
|
||||
|
||||
18
.github/workflows/test_dlt_connector.yml
vendored
18
.github/workflows/test_dlt_connector.yml
vendored
@ -60,15 +60,13 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Download Docker Image
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: docker-dlt-connector-test
|
||||
path: /tmp
|
||||
|
||||
- name: DLT-Connector | docker-compose mariadb
|
||||
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps mariadb
|
||||
|
||||
- name: Load Docker Image
|
||||
run: docker load < /tmp/dlt-connector.tar
|
||||
- name: Sleep for 30 seconds
|
||||
run: sleep 30s
|
||||
shell: bash
|
||||
|
||||
- name: Unit tests
|
||||
run: docker run --env NODE_ENV=test --rm gradido/dlt-connector:test yarn run test
|
||||
- name: DLT-Connector | Unit tests
|
||||
run: cd dlt-database && yarn && yarn build && cd ../dlt-connector && yarn && yarn test
|
||||
|
||||
@ -7,7 +7,7 @@ module.exports = {
|
||||
collectCoverageFrom: ['src/**/*.ts', '!**/node_modules/**', '!src/seeds/**', '!build/**'],
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
lines: 89,
|
||||
lines: 86,
|
||||
},
|
||||
},
|
||||
setupFiles: ['<rootDir>/test/testSetup.ts'],
|
||||
|
||||
@ -78,27 +78,34 @@ 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 | null): Promise<string> {
|
||||
if (transaction) {
|
||||
const typeString = getTransactionTypeString(transaction.typeId)
|
||||
const secondsSinceEpoch = Math.round(transaction.balanceDate.getTime() / 1000)
|
||||
const amountString = transaction.amount.toString()
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const { data } = await this.client.rawRequest(sendTransaction, {
|
||||
input: {
|
||||
type: typeString,
|
||||
amount: amountString,
|
||||
createdAt: secondsSinceEpoch,
|
||||
public async transmitTransaction(
|
||||
transaction: DbTransaction,
|
||||
senderCommunityUuid?: string,
|
||||
recipientCommunityUuid?: string,
|
||||
): Promise<string> {
|
||||
const typeString = getTransactionTypeString(transaction.typeId)
|
||||
const amountString = transaction.amount.toString()
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const { data } = await this.client.rawRequest(sendTransaction, {
|
||||
input: {
|
||||
senderUser: {
|
||||
uuid: transaction.userGradidoID,
|
||||
communityUuid: senderCommunityUuid,
|
||||
},
|
||||
})
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access
|
||||
return data.sendTransaction.dltTransactionIdHex
|
||||
} catch (e) {
|
||||
throw new LogError('Error send sending transaction to dlt-connector: ', e)
|
||||
}
|
||||
} else {
|
||||
throw new LogError('parameter transaction not set...')
|
||||
recipientUser: {
|
||||
uuid: transaction.linkedUserGradidoID,
|
||||
communityUuid: recipientCommunityUuid,
|
||||
},
|
||||
amount: amountString,
|
||||
type: typeString,
|
||||
createdAt: transaction.balanceDate.toString(),
|
||||
},
|
||||
})
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access
|
||||
return data.sendTransaction.dltTransactionIdHex
|
||||
} catch (e) {
|
||||
throw new LogError('Error send sending transaction to dlt-connector: ', e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,7 +19,7 @@ const constants = {
|
||||
LOG_LEVEL: process.env.LOG_LEVEL ?? 'info',
|
||||
CONFIG_VERSION: {
|
||||
DEFAULT: 'DEFAULT',
|
||||
EXPECTED: 'v18.2023-07-10',
|
||||
EXPECTED: 'v19.2023-08-25',
|
||||
CURRENT: '',
|
||||
},
|
||||
}
|
||||
@ -124,6 +124,11 @@ if (
|
||||
const federation = {
|
||||
FEDERATION_VALIDATE_COMMUNITY_TIMER:
|
||||
Number(process.env.FEDERATION_VALIDATE_COMMUNITY_TIMER) || 60000,
|
||||
// default value for community-uuid is equal uuid of stage-3
|
||||
FEDERATION_XCOM_RECEIVER_COMMUNITY_UUID:
|
||||
process.env.FEDERATION_XCOM_RECEIVER_COMMUNITY_UUID ?? '56a55482-909e-46a4-bfa2-cd025e894ebc',
|
||||
FEDERATION_XCOM_MAXREPEAT_REVERTSENDCOINS:
|
||||
process.env.FEDERATION_XCOM_MAXREPEAT_REVERTSENDCOINS ?? 3,
|
||||
}
|
||||
|
||||
export const CONFIG = {
|
||||
|
||||
93
backend/src/federation/client/1_0/SendCoinsClient.ts
Normal file
93
backend/src/federation/client/1_0/SendCoinsClient.ts
Normal file
@ -0,0 +1,93 @@
|
||||
import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity'
|
||||
import { GraphQLClient } from 'graphql-request'
|
||||
|
||||
import { LogError } from '@/server/LogError'
|
||||
import { backendLogger as logger } from '@/server/logger'
|
||||
|
||||
import { SendCoinsArgs } from './model/SendCoinsArgs'
|
||||
import { revertSendCoins } from './query/revertSendCoins'
|
||||
import { voteForSendCoins } from './query/voteForSendCoins'
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
export class SendCoinsClient {
|
||||
dbCom: DbFederatedCommunity
|
||||
endpoint: string
|
||||
client: GraphQLClient
|
||||
|
||||
constructor(dbCom: DbFederatedCommunity) {
|
||||
this.dbCom = dbCom
|
||||
this.endpoint = `${dbCom.endPoint.endsWith('/') ? dbCom.endPoint : dbCom.endPoint + '/'}${
|
||||
dbCom.apiVersion
|
||||
}/`
|
||||
this.client = new GraphQLClient(this.endpoint, {
|
||||
method: 'GET',
|
||||
jsonSerializer: {
|
||||
parse: JSON.parse,
|
||||
stringify: JSON.stringify,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
voteForSendCoins = async (args: SendCoinsArgs): Promise<string | undefined> => {
|
||||
logger.debug('X-Com: voteForSendCoins against endpoint=', this.endpoint)
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const { data } = await this.client.rawRequest(voteForSendCoins, { args })
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
if (!data?.voteForSendCoins?.voteForSendCoins) {
|
||||
logger.warn(
|
||||
'X-Com: voteForSendCoins failed with: ',
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
data.voteForSendCoins.voteForSendCoins,
|
||||
)
|
||||
return
|
||||
}
|
||||
logger.debug(
|
||||
'X-Com: voteForSendCoins successful with result=',
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
data.voteForSendCoins,
|
||||
)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access
|
||||
return data.voteForSendCoins.voteForSendCoins
|
||||
} catch (err) {
|
||||
throw new LogError(`X-Com: voteForSendCoins failed for endpoint=${this.endpoint}:`, err)
|
||||
}
|
||||
}
|
||||
|
||||
revertSendCoins = async (args: SendCoinsArgs): Promise<boolean> => {
|
||||
logger.debug('X-Com: revertSendCoins against endpoint=', this.endpoint)
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const { data } = await this.client.rawRequest(revertSendCoins, { args })
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
if (!data?.revertSendCoins?.revertSendCoins) {
|
||||
logger.warn('X-Com: revertSendCoins without response data from endpoint', this.endpoint)
|
||||
return false
|
||||
}
|
||||
logger.debug(`X-Com: revertSendCoins successful from endpoint=${this.endpoint}`)
|
||||
return true
|
||||
} catch (err) {
|
||||
logger.error(`X-Com: revertSendCoins failed for endpoint=${this.endpoint}`, err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
commitSendCoins = async (args: SendCoinsArgs): Promise<boolean> => {
|
||||
logger.debug(`X-Com: commitSendCoins against endpoint='${this.endpoint}'...`)
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const { data } = await this.client.rawRequest(commitSendCoins, { args })
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
if (!data?.commitSendCoins?.acknowledged) {
|
||||
logger.warn('X-Com: commitSendCoins without response data from endpoint', this.endpoint)
|
||||
return false
|
||||
}
|
||||
logger.debug(`X-Com: commitSendCoins successful from endpoint=${this.endpoint}`)
|
||||
return true
|
||||
} catch (err) {
|
||||
throw new LogError(`X-Com: commitSendCoins failed for endpoint=${this.endpoint}`, err)
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
29
backend/src/federation/client/1_0/model/SendCoinsArgs.ts
Normal file
29
backend/src/federation/client/1_0/model/SendCoinsArgs.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { Decimal } from 'decimal.js-light'
|
||||
import { ArgsType, Field } from 'type-graphql'
|
||||
|
||||
@ArgsType()
|
||||
export class SendCoinsArgs {
|
||||
@Field(() => String)
|
||||
communityReceiverIdentifier: string
|
||||
|
||||
@Field(() => String)
|
||||
userReceiverIdentifier: string
|
||||
|
||||
@Field(() => String)
|
||||
creationDate: string
|
||||
|
||||
@Field(() => Decimal)
|
||||
amount: Decimal
|
||||
|
||||
@Field(() => String)
|
||||
memo: string
|
||||
|
||||
@Field(() => String)
|
||||
communitySenderIdentifier: string
|
||||
|
||||
@Field(() => String)
|
||||
userSenderIdentifier: string
|
||||
|
||||
@Field(() => String)
|
||||
userSenderName: string
|
||||
}
|
||||
17
backend/src/federation/client/1_0/model/SendCoinsResult.ts
Normal file
17
backend/src/federation/client/1_0/model/SendCoinsResult.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { ArgsType, Field } from 'type-graphql'
|
||||
|
||||
@ArgsType()
|
||||
export class SendCoinsResult {
|
||||
constructor() {
|
||||
this.vote = false
|
||||
}
|
||||
|
||||
@Field(() => Boolean)
|
||||
vote: boolean
|
||||
|
||||
@Field(() => String)
|
||||
receiverFirstName: string
|
||||
|
||||
@Field(() => String)
|
||||
receiverLastName: string
|
||||
}
|
||||
25
backend/src/federation/client/1_0/query/revertSendCoins.ts
Normal file
25
backend/src/federation/client/1_0/query/revertSendCoins.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { gql } from 'graphql-request'
|
||||
|
||||
export const revertSendCoins = gql`
|
||||
mutation (
|
||||
$communityReceiverIdentifier: String!
|
||||
$userReceiverIdentifier: String!
|
||||
$creationDate: String!
|
||||
$amount: Decimal!
|
||||
$memo: String!
|
||||
$communitySenderIdentifier: String!
|
||||
$userSenderIdentifier: String!
|
||||
$userSenderName: String!
|
||||
) {
|
||||
revertSendCoins(
|
||||
communityReceiverIdentifier: $communityReceiverIdentifier
|
||||
userReceiverIdentifier: $userReceiverIdentifier
|
||||
creationDate: $creationDate
|
||||
amount: $amount
|
||||
memo: $memo
|
||||
communitySenderIdentifier: $communitySenderIdentifier
|
||||
userSenderIdentifier: $userSenderIdentifier
|
||||
userSenderName: $userSenderName
|
||||
)
|
||||
}
|
||||
`
|
||||
25
backend/src/federation/client/1_0/query/voteForSendCoins.ts
Normal file
25
backend/src/federation/client/1_0/query/voteForSendCoins.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { gql } from 'graphql-request'
|
||||
|
||||
export const voteForSendCoins = gql`
|
||||
mutation (
|
||||
$communityReceiverIdentifier: String!
|
||||
$userReceiverIdentifier: String!
|
||||
$creationDate: String!
|
||||
$amount: Decimal!
|
||||
$memo: String!
|
||||
$communitySenderIdentifier: String!
|
||||
$userSenderIdentifier: String!
|
||||
$userSenderName: String!
|
||||
) {
|
||||
voteForSendCoins(
|
||||
communityReceiverIdentifier: $communityReceiverIdentifier
|
||||
userReceiverIdentifier: $userReceiverIdentifier
|
||||
creationDate: $creationDate
|
||||
amount: $amount
|
||||
memo: $memo
|
||||
communitySenderIdentifier: $communitySenderIdentifier
|
||||
userSenderIdentifier: $userSenderIdentifier
|
||||
userSenderName: $userSenderName
|
||||
)
|
||||
}
|
||||
`
|
||||
5
backend/src/federation/client/1_1/SendCoinsClient.ts
Normal file
5
backend/src/federation/client/1_1/SendCoinsClient.ts
Normal file
@ -0,0 +1,5 @@
|
||||
// eslint-disable-next-line camelcase
|
||||
import { SendCoinsClient as V1_0_SendCoinsClient } from '@/federation/client/1_0/SendCoinsClient'
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
export class SendCoinsClient extends V1_0_SendCoinsClient {}
|
||||
62
backend/src/federation/client/SendCoinsClientFactory.ts
Normal file
62
backend/src/federation/client/SendCoinsClientFactory.ts
Normal file
@ -0,0 +1,62 @@
|
||||
import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity'
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
import { SendCoinsClient as V1_0_SendCoinsClient } from '@/federation/client/1_0/SendCoinsClient'
|
||||
// eslint-disable-next-line camelcase
|
||||
import { SendCoinsClient as V1_1_SendCoinsClient } from '@/federation/client/1_1/SendCoinsClient'
|
||||
import { ApiVersionType } from '@/federation/enum/apiVersionType'
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
type SendCoinsClient = V1_0_SendCoinsClient | V1_1_SendCoinsClient
|
||||
|
||||
interface SendCoinsClientInstance {
|
||||
id: number
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
client: SendCoinsClient
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-extraneous-class
|
||||
export class SendCoinsClientFactory {
|
||||
private static instanceArray: SendCoinsClientInstance[] = []
|
||||
|
||||
/**
|
||||
* The Singleton's constructor should always be private to prevent direct
|
||||
* construction calls with the `new` operator.
|
||||
*/
|
||||
// eslint-disable-next-line no-useless-constructor, @typescript-eslint/no-empty-function
|
||||
private constructor() {}
|
||||
|
||||
private static createSendCoinsClient = (dbCom: DbFederatedCommunity) => {
|
||||
switch (dbCom.apiVersion) {
|
||||
case ApiVersionType.V1_0:
|
||||
return new V1_0_SendCoinsClient(dbCom)
|
||||
case ApiVersionType.V1_1:
|
||||
return new V1_1_SendCoinsClient(dbCom)
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The static method that controls the access to the singleton instance.
|
||||
*
|
||||
* This implementation let you subclass the Singleton class while keeping
|
||||
* just one instance of each subclass around.
|
||||
*/
|
||||
public static getInstance(dbCom: DbFederatedCommunity): SendCoinsClient | null {
|
||||
const instance = SendCoinsClientFactory.instanceArray.find(
|
||||
(instance) => instance.id === dbCom.id,
|
||||
)
|
||||
if (instance) {
|
||||
return instance.client
|
||||
}
|
||||
const client = SendCoinsClientFactory.createSendCoinsClient(dbCom)
|
||||
if (client) {
|
||||
SendCoinsClientFactory.instanceArray.push({
|
||||
id: dbCom.id,
|
||||
client,
|
||||
} as SendCoinsClientInstance)
|
||||
}
|
||||
return client
|
||||
}
|
||||
}
|
||||
116
backend/src/graphql/resolver/util/processXComSendCoins.ts
Normal file
116
backend/src/graphql/resolver/util/processXComSendCoins.ts
Normal file
@ -0,0 +1,116 @@
|
||||
import { Community as DbCommunity } from '@entity/Community'
|
||||
import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity'
|
||||
import { PendingTransaction as DbPendingTransaction } from '@entity/PendingTransaction'
|
||||
import { User as dbUser } from '@entity/User'
|
||||
import { Decimal } from 'decimal.js-light'
|
||||
|
||||
import { CONFIG } from '@/config'
|
||||
import { SendCoinsArgs } from '@/federation/client/1_0/model/SendCoinsArgs'
|
||||
// eslint-disable-next-line camelcase
|
||||
import { SendCoinsClient as V1_0_SendCoinsClient } from '@/federation/client/1_0/SendCoinsClient'
|
||||
import { SendCoinsClientFactory } from '@/federation/client/SendCoinsClientFactory'
|
||||
import { PendingTransactionState } from '@/graphql/enum/PendingTransactionState'
|
||||
import { TransactionTypeId } from '@/graphql/enum/TransactionTypeId'
|
||||
import { LogError } from '@/server/LogError'
|
||||
import { backendLogger as logger } from '@/server/logger'
|
||||
import { calculateSenderBalance } from '@/util/calculateSenderBalance'
|
||||
import { fullName } from '@/util/utilities'
|
||||
|
||||
export async function processXComPendingSendCoins(
|
||||
receiverFCom: DbFederatedCommunity,
|
||||
receiverCom: DbCommunity,
|
||||
senderCom: DbCommunity,
|
||||
creationDate: Date,
|
||||
amount: Decimal,
|
||||
memo: string,
|
||||
sender: dbUser,
|
||||
recipient: dbUser,
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
logger.debug(
|
||||
`XCom: processXComPendingSendCoins...`,
|
||||
receiverFCom,
|
||||
receiverCom,
|
||||
senderCom,
|
||||
creationDate,
|
||||
amount,
|
||||
memo,
|
||||
sender,
|
||||
recipient,
|
||||
)
|
||||
// first calculate the sender balance and check if the transaction is allowed
|
||||
const senderBalance = await calculateSenderBalance(sender.id, amount.mul(-1), creationDate)
|
||||
if (!senderBalance) {
|
||||
throw new LogError('User has not enough GDD or amount is < 0', senderBalance)
|
||||
}
|
||||
logger.debug(`X-Com: calculated senderBalance = `, senderBalance)
|
||||
|
||||
const client = SendCoinsClientFactory.getInstance(receiverFCom)
|
||||
// eslint-disable-next-line camelcase
|
||||
if (client instanceof V1_0_SendCoinsClient) {
|
||||
const args = new SendCoinsArgs()
|
||||
args.communityReceiverIdentifier = receiverCom.communityUuid
|
||||
? receiverCom.communityUuid
|
||||
: CONFIG.FEDERATION_XCOM_RECEIVER_COMMUNITY_UUID
|
||||
args.userReceiverIdentifier = recipient.gradidoID
|
||||
args.creationDate = creationDate.toISOString()
|
||||
args.amount = amount
|
||||
args.memo = memo
|
||||
args.communitySenderIdentifier = senderCom.communityUuid
|
||||
? senderCom.communityUuid
|
||||
: 'homeCom-UUID'
|
||||
args.userSenderIdentifier = sender.gradidoID
|
||||
args.userSenderName = fullName(sender.firstName, sender.lastName)
|
||||
logger.debug(`X-Com: ready for voteForSendCoins with args=`, args)
|
||||
const recipientName = await client.voteForSendCoins(args)
|
||||
logger.debug(`X-Com: returnd from voteForSendCoins:`, recipientName)
|
||||
if (recipientName) {
|
||||
// writing the pending transaction on receiver-side was successfull, so now write the sender side
|
||||
try {
|
||||
const pendingTx = DbPendingTransaction.create()
|
||||
pendingTx.amount = amount.mul(-1)
|
||||
pendingTx.balance = senderBalance ? senderBalance.balance : new Decimal(0)
|
||||
pendingTx.balanceDate = creationDate
|
||||
pendingTx.decay = senderBalance ? senderBalance.decay.decay : new Decimal(0)
|
||||
pendingTx.decayStart = senderBalance ? senderBalance.decay.start : null
|
||||
pendingTx.linkedUserCommunityUuid = receiverCom.communityUuid
|
||||
? receiverCom.communityUuid
|
||||
: CONFIG.FEDERATION_XCOM_RECEIVER_COMMUNITY_UUID
|
||||
pendingTx.linkedUserGradidoID = recipient.gradidoID
|
||||
pendingTx.linkedUserName = recipientName
|
||||
pendingTx.memo = memo
|
||||
pendingTx.previous = senderBalance ? senderBalance.lastTransactionId : null
|
||||
pendingTx.state = PendingTransactionState.NEW
|
||||
pendingTx.typeId = TransactionTypeId.SEND
|
||||
if (senderCom.communityUuid) pendingTx.userCommunityUuid = senderCom.communityUuid
|
||||
pendingTx.userGradidoID = sender.gradidoID
|
||||
pendingTx.userName = fullName(sender.firstName, sender.lastName)
|
||||
logger.debug(`X-Com: initialized sender pendingTX=`, pendingTx)
|
||||
|
||||
await DbPendingTransaction.insert(pendingTx)
|
||||
logger.debug(`X-Com: sender pendingTx successfully inserted...`)
|
||||
} catch (err) {
|
||||
logger.error(`Error in writing sender pending transaction: `, err)
|
||||
// revert the existing pending transaction on receiver side
|
||||
let revertCount = 0
|
||||
logger.debug(`X-Com: first try to revertSendCoins of receiver`)
|
||||
do {
|
||||
if (await client.revertSendCoins(args)) {
|
||||
logger.debug(`revertSendCoins()-1_0... successfull after revertCount=`, revertCount)
|
||||
// treat revertingSendCoins as an error of the whole sendCoins-process
|
||||
throw new LogError('Error in writing sender pending transaction: `, err')
|
||||
}
|
||||
} while (CONFIG.FEDERATION_XCOM_MAXREPEAT_REVERTSENDCOINS > revertCount++)
|
||||
throw new LogError(
|
||||
`Error in reverting receiver pending transaction even after revertCount=`,
|
||||
revertCount,
|
||||
)
|
||||
}
|
||||
logger.debug(`voteForSendCoins()-1_0... successfull`)
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(`Error:`, err)
|
||||
}
|
||||
return true
|
||||
}
|
||||
@ -6,6 +6,7 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
|
||||
import { Connection } from '@dbTools/typeorm'
|
||||
import { Community } from '@entity/Community'
|
||||
import { DltTransaction } from '@entity/DltTransaction'
|
||||
import { Transaction } from '@entity/Transaction'
|
||||
import { ApolloServerTestClient } from 'apollo-server-testing'
|
||||
@ -14,6 +15,7 @@ import { Decimal } from 'decimal.js-light'
|
||||
// import { Response } from 'graphql-request/dist/types'
|
||||
import { GraphQLClient } from 'graphql-request'
|
||||
import { Response } from 'graphql-request/dist/types'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
import { testEnvironment, cleanDB } from '@test/helpers'
|
||||
import { logger, i18n as localization } from '@test/testSetup'
|
||||
@ -80,6 +82,16 @@ let testEnv: {
|
||||
}
|
||||
*/
|
||||
|
||||
async function createHomeCommunity(): Promise<Community> {
|
||||
const homeCommunity = Community.create()
|
||||
homeCommunity.foreign = false
|
||||
homeCommunity.communityUuid = uuidv4()
|
||||
homeCommunity.url = 'localhost'
|
||||
homeCommunity.publicKey = Buffer.from('0x6e6a6c6d6feffe', 'hex')
|
||||
await Community.save(homeCommunity)
|
||||
return homeCommunity
|
||||
}
|
||||
|
||||
async function createTxCREATION1(verified: boolean): Promise<Transaction> {
|
||||
let tx = Transaction.create()
|
||||
tx.amount = new Decimal(1000)
|
||||
@ -358,6 +370,7 @@ describe('create and send Transactions to DltConnector', () => {
|
||||
txCREATION1 = await createTxCREATION1(false)
|
||||
txCREATION2 = await createTxCREATION2(false)
|
||||
txCREATION3 = await createTxCREATION3(false)
|
||||
await createHomeCommunity()
|
||||
|
||||
CONFIG.DLT_CONNECTOR = false
|
||||
await sendTransactionsToDltConnector()
|
||||
@ -413,6 +426,7 @@ describe('create and send Transactions to DltConnector', () => {
|
||||
txCREATION1 = await createTxCREATION1(false)
|
||||
txCREATION2 = await createTxCREATION2(false)
|
||||
txCREATION3 = await createTxCREATION3(false)
|
||||
await createHomeCommunity()
|
||||
|
||||
CONFIG.DLT_CONNECTOR = true
|
||||
|
||||
@ -481,6 +495,7 @@ describe('create and send Transactions to DltConnector', () => {
|
||||
txCREATION1 = await createTxCREATION1(true)
|
||||
txCREATION2 = await createTxCREATION2(true)
|
||||
txCREATION3 = await createTxCREATION3(true)
|
||||
await createHomeCommunity()
|
||||
|
||||
txSEND1to2 = await createTxSend1ToReceive2(false)
|
||||
txRECEIVE2From1 = await createTxReceive2FromSend1(false)
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { IsNull } from '@dbTools/typeorm'
|
||||
import { Community } from '@entity/Community'
|
||||
import { DltTransaction } from '@entity/DltTransaction'
|
||||
import { Transaction } from '@entity/Transaction'
|
||||
|
||||
@ -16,6 +17,13 @@ export async function sendTransactionsToDltConnector(): Promise<void> {
|
||||
try {
|
||||
await createDltTransactions()
|
||||
const dltConnector = DltConnectorClient.getInstance()
|
||||
// TODO: get actual communities from users
|
||||
const homeCommunity = await Community.findOneOrFail({ where: { foreign: false } })
|
||||
const senderCommunityUuid = homeCommunity.communityUuid
|
||||
if (!senderCommunityUuid) {
|
||||
throw new Error('Cannot find community uuid of home community')
|
||||
}
|
||||
const recipientCommunityUuid = ''
|
||||
if (dltConnector) {
|
||||
logger.debug('with sending to DltConnector...')
|
||||
const dltTransactions = await DltTransaction.find({
|
||||
@ -23,9 +31,17 @@ export async function sendTransactionsToDltConnector(): Promise<void> {
|
||||
relations: ['transaction'],
|
||||
order: { createdAt: 'ASC', id: 'ASC' },
|
||||
})
|
||||
|
||||
for (const dltTx of dltTransactions) {
|
||||
if (!dltTx.transaction) {
|
||||
continue
|
||||
}
|
||||
try {
|
||||
const messageId = await dltConnector.transmitTransaction(dltTx.transaction)
|
||||
const messageId = await dltConnector.transmitTransaction(
|
||||
dltTx.transaction,
|
||||
senderCommunityUuid,
|
||||
recipientCommunityUuid,
|
||||
)
|
||||
const dltMessageId = Buffer.from(messageId, 'hex')
|
||||
if (dltMessageId.length !== 32) {
|
||||
logger.error(
|
||||
|
||||
21
backend/src/util/calculateSenderBalance.ts
Normal file
21
backend/src/util/calculateSenderBalance.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { Decimal } from 'decimal.js-light'
|
||||
|
||||
import { Decay } from '@model/Decay'
|
||||
|
||||
import { getLastTransaction } from '@/graphql/resolver/util/getLastTransaction'
|
||||
|
||||
import { calculateDecay } from './decay'
|
||||
|
||||
export async function calculateSenderBalance(
|
||||
userId: number,
|
||||
amount: Decimal,
|
||||
time: Date,
|
||||
): Promise<{ balance: Decimal; decay: Decay; lastTransactionId: number } | null> {
|
||||
const lastTransaction = await getLastTransaction(userId)
|
||||
if (!lastTransaction) return null
|
||||
|
||||
const decay = calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, time)
|
||||
|
||||
const balance = decay.balance.add(amount.toString())
|
||||
return { balance, lastTransactionId: lastTransaction.id, decay }
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
CONFIG_VERSION=v2.2023-07-07
|
||||
CONFIG_VERSION=v4.2023-09-12
|
||||
|
||||
# SET LOG LEVEL AS NEEDED IN YOUR .ENV
|
||||
# POSSIBLE VALUES: all | trace | debug | info | warn | error | fatal
|
||||
@ -7,6 +7,16 @@ CONFIG_VERSION=v2.2023-07-07
|
||||
# IOTA
|
||||
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=6000
|
||||
DLT_CONNECTOR_PORT=6010
|
||||
@ -3,6 +3,16 @@ CONFIG_VERSION=$DLT_CONNECTOR_CONFIG_VERSION
|
||||
#IOTA
|
||||
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
|
||||
@ -16,7 +16,7 @@ ENV BUILD_COMMIT="0000000"
|
||||
## SET NODE_ENV
|
||||
ENV NODE_ENV="production"
|
||||
## App relevant Envs
|
||||
ENV PORT="6000"
|
||||
ENV PORT="6010"
|
||||
|
||||
# Labels
|
||||
LABEL org.label-schema.build-date="${BUILD_DATE}"
|
||||
@ -44,6 +44,7 @@ EXPOSE ${PORT}
|
||||
RUN mkdir -p ${DOCKER_WORKDIR}
|
||||
WORKDIR ${DOCKER_WORKDIR}
|
||||
|
||||
RUN mkdir -p /dlt-database
|
||||
|
||||
##################################################################################
|
||||
# DEVELOPMENT (Connected to the local environment, to reload on demand) ##########
|
||||
@ -56,7 +57,7 @@ FROM base as development
|
||||
# Run command
|
||||
# (for development we need to execute yarn install since the
|
||||
# node_modules are on another volume and need updating)
|
||||
CMD /bin/sh -c "cd /app && yarn install && yarn run dev"
|
||||
CMD /bin/sh -c "cd /dlt-database && yarn install && yarn build && cd /app && yarn install && yarn run dev"
|
||||
|
||||
##################################################################################
|
||||
# BUILD (Does contain all files and is therefore bloated) ########################
|
||||
@ -65,13 +66,21 @@ FROM base as build
|
||||
|
||||
# Copy everything from dlt-connector
|
||||
COPY ./dlt-connector/ ./
|
||||
# Copy everything from dlt-database
|
||||
COPY ./dlt-database/ ../dlt-database/
|
||||
|
||||
# yarn install dlt-connector
|
||||
RUN yarn install --production=false --frozen-lockfile --non-interactive
|
||||
|
||||
# yarn install dlt-database
|
||||
RUN cd ../dlt-database && yarn install --production=false --frozen-lockfile --non-interactive
|
||||
|
||||
# yarn build
|
||||
RUN yarn run build
|
||||
|
||||
# yarn build dlt-database
|
||||
RUN cd ../dlt-database && yarn run build
|
||||
|
||||
##################################################################################
|
||||
# TEST ###########################################################################
|
||||
##################################################################################
|
||||
@ -90,9 +99,10 @@ RUN apk del rust cargo python3 make g++
|
||||
|
||||
# Copy "binary"-files from build image
|
||||
COPY --from=build ${DOCKER_WORKDIR}/build ./build
|
||||
|
||||
COPY --from=build ${DOCKER_WORKDIR}/../dlt-database/build ../dlt-database/build
|
||||
# We also copy the node_modules express and serve-static for the run script
|
||||
COPY --from=build ${DOCKER_WORKDIR}/node_modules ./node_modules
|
||||
COPY --from=build ${DOCKER_WORKDIR}/../dlt-database/node_modules ../dlt-database/node_modules
|
||||
# Copy static files
|
||||
# COPY --from=build ${DOCKER_WORKDIR}/public ./public
|
||||
# Copy package.json for script definitions (lock file should not be needed)
|
||||
|
||||
@ -6,7 +6,7 @@ module.exports = {
|
||||
collectCoverageFrom: ['src/**/*.ts', '!**/node_modules/**', '!src/seeds/**', '!build/**'],
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
lines: 63,
|
||||
lines: 77,
|
||||
},
|
||||
},
|
||||
setupFiles: ['<rootDir>/test/testSetup.ts'],
|
||||
@ -14,22 +14,26 @@ module.exports = {
|
||||
modulePathIgnorePatterns: ['<rootDir>/build/'],
|
||||
moduleNameMapper: {
|
||||
'@/(.*)': '<rootDir>/src/$1',
|
||||
'@arg/(.*)': '<rootDir>/src/graphql/arg/$1',
|
||||
'@controller/(.*)': '<rootDir>/src/controller/$1',
|
||||
'@enum/(.*)': '<rootDir>/src/graphql/enum/$1',
|
||||
'@resolver/(.*)': '<rootDir>/src/graphql/resolver/$1',
|
||||
'@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>/../database/entity/$1'
|
||||
: '<rootDir>/../database/build/entity/$1',
|
||||
? '<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>/../database/src/$1'
|
||||
: '<rootDir>/../database/build/src/$1',
|
||||
? '<rootDir>/../dlt-database/src/$1'
|
||||
: '<rootDir>/../dlt-database/build/src/$1',
|
||||
'@validator/(.*)': '<rootDir>/src/graphql/validator/$1',
|
||||
},
|
||||
}
|
||||
/*
|
||||
|
||||
@ -25,6 +25,7 @@
|
||||
"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",
|
||||
"graphql": "^16.7.1",
|
||||
@ -32,6 +33,7 @@
|
||||
"log4js": "^6.7.1",
|
||||
"nodemon": "^2.0.20",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"sodium-native": "^4.0.4",
|
||||
"tsconfig-paths": "^4.1.2",
|
||||
"type-graphql": "^2.0.0-beta.2"
|
||||
},
|
||||
@ -41,6 +43,7 @@
|
||||
"@types/cors": "^2.8.13",
|
||||
"@types/jest": "^27.0.2",
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/sodium-native": "^2.3.5",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"@typescript-eslint/eslint-plugin": "^5.57.1",
|
||||
"@typescript-eslint/parser": "^5.57.1",
|
||||
@ -58,6 +61,8 @@
|
||||
"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": {
|
||||
|
||||
@ -4,11 +4,12 @@ dotenv.config()
|
||||
|
||||
const constants = {
|
||||
LOG4JS_CONFIG: 'log4js-config.json',
|
||||
DB_VERSION: '0002-refactor_add_community',
|
||||
// default log level on production should be info
|
||||
LOG_LEVEL: process.env.LOG_LEVEL || 'info',
|
||||
CONFIG_VERSION: {
|
||||
DEFAULT: 'DEFAULT',
|
||||
EXPECTED: 'v2.2023-07-07',
|
||||
EXPECTED: 'v4.2023-09-12',
|
||||
CURRENT: '',
|
||||
},
|
||||
}
|
||||
@ -17,13 +18,24 @@ const server = {
|
||||
PRODUCTION: process.env.NODE_ENV === 'production' || false,
|
||||
}
|
||||
|
||||
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 ?? null,
|
||||
}
|
||||
|
||||
const dltConnector = {
|
||||
DLT_CONNECTOR_PORT: process.env.DLT_CONNECTOR_PORT || 6000,
|
||||
DLT_CONNECTOR_PORT: process.env.DLT_CONNECTOR_PORT || 6010,
|
||||
}
|
||||
|
||||
// Check config version
|
||||
@ -41,6 +53,7 @@ if (
|
||||
export const CONFIG = {
|
||||
...constants,
|
||||
...server,
|
||||
...database,
|
||||
...iota,
|
||||
...dltConnector,
|
||||
}
|
||||
|
||||
66
dlt-connector/src/controller/Community.test.ts
Normal file
66
dlt-connector/src/controller/Community.test.ts
Normal file
@ -0,0 +1,66 @@
|
||||
import 'reflect-metadata'
|
||||
import { CommunityDraft } from '@/graphql/input/CommunityDraft'
|
||||
import { create as createCommunity, getAllTopics, isExist } from './Community'
|
||||
import { TestDB } from '@test/TestDB'
|
||||
import { getDataSource } from '@/typeorm/DataSource'
|
||||
import { Community } from '@entity/Community'
|
||||
import { iotaTopicFromCommunityUUID } from '@/utils/typeConverter'
|
||||
|
||||
jest.mock('@typeorm/DataSource', () => ({
|
||||
getDataSource: () => TestDB.instance.dbConnect,
|
||||
}))
|
||||
|
||||
describe('controller/Community', () => {
|
||||
beforeAll(async () => {
|
||||
await TestDB.instance.setupTestDB()
|
||||
// apolloTestServer = await createApolloTestServer()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await TestDB.instance.teardownTestDB()
|
||||
})
|
||||
|
||||
describe('createCommunity', () => {
|
||||
it('valid community', async () => {
|
||||
const communityDraft = new CommunityDraft()
|
||||
communityDraft.foreign = false
|
||||
communityDraft.createdAt = '2022-05-01T17:00:12.128Z'
|
||||
communityDraft.uuid = '3d813cbb-47fb-32ba-91df-831e1593ac29'
|
||||
|
||||
const iotaTopic = iotaTopicFromCommunityUUID(communityDraft.uuid)
|
||||
expect(iotaTopic).toEqual('204ef6aed15fbf0f9da5819e88f8eea8e3adbe1e2c2d43280780a4b8c2d32b56')
|
||||
|
||||
const createdAtDate = new Date(communityDraft.createdAt)
|
||||
const communityEntity = createCommunity(communityDraft)
|
||||
expect(communityEntity).toMatchObject({
|
||||
iotaTopic,
|
||||
createdAt: createdAtDate,
|
||||
foreign: false,
|
||||
})
|
||||
await getDataSource().manager.save(communityEntity)
|
||||
})
|
||||
})
|
||||
|
||||
describe('list communities', () => {
|
||||
it('get all topics', async () => {
|
||||
expect(await getAllTopics()).toMatchObject([
|
||||
'204ef6aed15fbf0f9da5819e88f8eea8e3adbe1e2c2d43280780a4b8c2d32b56',
|
||||
])
|
||||
})
|
||||
|
||||
it('isExist with communityDraft', async () => {
|
||||
const communityDraft = new CommunityDraft()
|
||||
communityDraft.foreign = false
|
||||
communityDraft.createdAt = '2022-05-01T17:00:12.128Z'
|
||||
communityDraft.uuid = '3d813cbb-47fb-32ba-91df-831e1593ac29'
|
||||
expect(await isExist(communityDraft)).toBe(true)
|
||||
})
|
||||
|
||||
it('createdAt with ms precision', async () => {
|
||||
const list = await Community.findOne({ where: { foreign: false } })
|
||||
expect(list).toMatchObject({
|
||||
createdAt: new Date('2022-05-01T17:00:12.128Z'),
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
28
dlt-connector/src/controller/Community.ts
Normal file
28
dlt-connector/src/controller/Community.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { CommunityDraft } from '@/graphql/input/CommunityDraft'
|
||||
import { iotaTopicFromCommunityUUID } from '@/utils/typeConverter'
|
||||
import { Community } from '@entity/Community'
|
||||
|
||||
export const isExist = async (community: CommunityDraft | string): Promise<boolean> => {
|
||||
const iotaTopic =
|
||||
community instanceof CommunityDraft ? iotaTopicFromCommunityUUID(community.uuid) : community
|
||||
const result = await Community.find({
|
||||
where: { iotaTopic },
|
||||
})
|
||||
return result.length > 0
|
||||
}
|
||||
|
||||
export const create = (community: CommunityDraft, topic?: string): Community => {
|
||||
const communityEntity = Community.create()
|
||||
communityEntity.iotaTopic = topic ?? iotaTopicFromCommunityUUID(community.uuid)
|
||||
communityEntity.createdAt = new Date(community.createdAt)
|
||||
communityEntity.foreign = community.foreign
|
||||
if (!community.foreign) {
|
||||
// TODO: generate keys
|
||||
}
|
||||
return communityEntity
|
||||
}
|
||||
|
||||
export const getAllTopics = async (): Promise<string[]> => {
|
||||
const communities = await Community.find({ select: { iotaTopic: true } })
|
||||
return communities.map((community) => community.iotaTopic)
|
||||
}
|
||||
10
dlt-connector/src/controller/GradidoTransaction.ts
Normal file
10
dlt-connector/src/controller/GradidoTransaction.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { GradidoTransaction } from '@/proto/3_3/GradidoTransaction'
|
||||
import { TransactionBody } from '@/proto/3_3/TransactionBody'
|
||||
|
||||
export const create = (body: TransactionBody): GradidoTransaction => {
|
||||
const transaction = new GradidoTransaction({
|
||||
bodyBytes: Buffer.from(TransactionBody.encode(body).finish()),
|
||||
})
|
||||
// TODO: add correct signature(s)
|
||||
return transaction
|
||||
}
|
||||
6
dlt-connector/src/controller/TransactionBase.ts
Normal file
6
dlt-connector/src/controller/TransactionBase.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { TransactionValidationLevel } from '@/graphql/enum/TransactionValidationLevel'
|
||||
|
||||
export abstract class TransactionBase {
|
||||
// validate if transaction is valid, maybe expensive because depending on level several transactions will be fetched from db
|
||||
public abstract validate(level: TransactionValidationLevel): boolean
|
||||
}
|
||||
162
dlt-connector/src/controller/TransactionBody.test.ts
Normal file
162
dlt-connector/src/controller/TransactionBody.test.ts
Normal file
@ -0,0 +1,162 @@
|
||||
import 'reflect-metadata'
|
||||
import { TransactionDraft } from '@/graphql/input/TransactionDraft'
|
||||
import { create, determineCrossGroupType, determineOtherGroup } from './TransactionBody'
|
||||
import { UserIdentifier } from '@/graphql/input/UserIdentifier'
|
||||
import { TransactionError } from '@/graphql/model/TransactionError'
|
||||
import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType'
|
||||
import { CrossGroupType } from '@/graphql/enum/CrossGroupType'
|
||||
import { TransactionType } from '@/graphql/enum/TransactionType'
|
||||
import Decimal from 'decimal.js-light'
|
||||
|
||||
describe('test controller/TransactionBody', () => {
|
||||
describe('test create ', () => {
|
||||
const senderUser = new UserIdentifier()
|
||||
const recipientUser = new UserIdentifier()
|
||||
it('test with contribution transaction', () => {
|
||||
const transactionDraft = new TransactionDraft()
|
||||
transactionDraft.senderUser = senderUser
|
||||
transactionDraft.recipientUser = recipientUser
|
||||
transactionDraft.type = TransactionType.CREATION
|
||||
transactionDraft.amount = new Decimal(1000)
|
||||
transactionDraft.createdAt = '2022-01-02T19:10:34.121'
|
||||
transactionDraft.targetDate = '2021-12-01T10:05:00.191'
|
||||
const body = create(transactionDraft)
|
||||
|
||||
expect(body.creation).toBeDefined()
|
||||
expect(body).toMatchObject({
|
||||
createdAt: {
|
||||
seconds: 1641150634,
|
||||
nanoSeconds: 121000000,
|
||||
},
|
||||
versionNumber: '3.3',
|
||||
type: CrossGroupType.LOCAL,
|
||||
otherGroup: '',
|
||||
creation: {
|
||||
recipient: {
|
||||
amount: '1000',
|
||||
},
|
||||
targetDate: {
|
||||
seconds: 1638353100,
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
it('test with local send transaction send part', () => {
|
||||
const transactionDraft = new TransactionDraft()
|
||||
transactionDraft.senderUser = senderUser
|
||||
transactionDraft.recipientUser = recipientUser
|
||||
transactionDraft.type = TransactionType.SEND
|
||||
transactionDraft.amount = new Decimal(1000)
|
||||
transactionDraft.createdAt = '2022-01-02T19:10:34.121'
|
||||
const body = create(transactionDraft)
|
||||
|
||||
expect(body.transfer).toBeDefined()
|
||||
expect(body).toMatchObject({
|
||||
createdAt: {
|
||||
seconds: 1641150634,
|
||||
nanoSeconds: 121000000,
|
||||
},
|
||||
versionNumber: '3.3',
|
||||
type: CrossGroupType.LOCAL,
|
||||
otherGroup: '',
|
||||
transfer: {
|
||||
sender: {
|
||||
amount: '1000',
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('test with local send transaction receive part', () => {
|
||||
const transactionDraft = new TransactionDraft()
|
||||
transactionDraft.senderUser = senderUser
|
||||
transactionDraft.recipientUser = recipientUser
|
||||
transactionDraft.type = TransactionType.RECEIVE
|
||||
transactionDraft.amount = new Decimal(1000)
|
||||
transactionDraft.createdAt = '2022-01-02T19:10:34.121'
|
||||
const body = create(transactionDraft)
|
||||
|
||||
expect(body.transfer).toBeDefined()
|
||||
expect(body).toMatchObject({
|
||||
createdAt: {
|
||||
seconds: 1641150634,
|
||||
nanoSeconds: 121000000,
|
||||
},
|
||||
versionNumber: '3.3',
|
||||
type: CrossGroupType.LOCAL,
|
||||
otherGroup: '',
|
||||
transfer: {
|
||||
sender: {
|
||||
amount: '1000',
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
describe('test determineCrossGroupType', () => {
|
||||
const transactionDraft = new TransactionDraft()
|
||||
transactionDraft.senderUser = new UserIdentifier()
|
||||
transactionDraft.recipientUser = new UserIdentifier()
|
||||
|
||||
it('local transaction', () => {
|
||||
expect(determineCrossGroupType(transactionDraft)).toEqual(CrossGroupType.LOCAL)
|
||||
})
|
||||
|
||||
it('test with with invalid input', () => {
|
||||
transactionDraft.recipientUser.communityUuid = 'a72a4a4a-aa12-4f6c-b3d8-7cc65c67e24a'
|
||||
expect(() => determineCrossGroupType(transactionDraft)).toThrow(
|
||||
new TransactionError(
|
||||
TransactionErrorType.NOT_IMPLEMENTED_YET,
|
||||
'cannot determine CrossGroupType',
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
it('inbound transaction (send to sender community)', () => {
|
||||
transactionDraft.type = TransactionType.SEND
|
||||
expect(determineCrossGroupType(transactionDraft)).toEqual(CrossGroupType.INBOUND)
|
||||
})
|
||||
|
||||
it('outbound transaction (send to recipient community)', () => {
|
||||
transactionDraft.type = TransactionType.RECEIVE
|
||||
expect(determineCrossGroupType(transactionDraft)).toEqual(CrossGroupType.OUTBOUND)
|
||||
})
|
||||
})
|
||||
|
||||
describe('test determineOtherGroup', () => {
|
||||
const transactionDraft = new TransactionDraft()
|
||||
transactionDraft.senderUser = new UserIdentifier()
|
||||
transactionDraft.recipientUser = new UserIdentifier()
|
||||
|
||||
it('for inbound transaction, other group is from recipient, missing community id for recipient', () => {
|
||||
expect(() => determineOtherGroup(CrossGroupType.INBOUND, transactionDraft)).toThrowError(
|
||||
new TransactionError(
|
||||
TransactionErrorType.MISSING_PARAMETER,
|
||||
'missing recipient user community id for cross group transaction',
|
||||
),
|
||||
)
|
||||
})
|
||||
it('for inbound transaction, other group is from recipient', () => {
|
||||
transactionDraft.recipientUser.communityUuid = 'b8e9f00a-5a56-4b23-8c44-6823ac9e0d2d'
|
||||
expect(determineOtherGroup(CrossGroupType.INBOUND, transactionDraft)).toEqual(
|
||||
'b8e9f00a-5a56-4b23-8c44-6823ac9e0d2d',
|
||||
)
|
||||
})
|
||||
|
||||
it('for outbound transaction, other group is from sender, missing community id for sender', () => {
|
||||
expect(() => determineOtherGroup(CrossGroupType.OUTBOUND, transactionDraft)).toThrowError(
|
||||
new TransactionError(
|
||||
TransactionErrorType.MISSING_PARAMETER,
|
||||
'missing sender user community id for cross group transaction',
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
it('for outbound transaction, other group is from sender', () => {
|
||||
transactionDraft.senderUser.communityUuid = 'a72a4a4a-aa12-4f6c-b3d8-7cc65c67e24a'
|
||||
expect(determineOtherGroup(CrossGroupType.OUTBOUND, transactionDraft)).toEqual(
|
||||
'a72a4a4a-aa12-4f6c-b3d8-7cc65c67e24a',
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
74
dlt-connector/src/controller/TransactionBody.ts
Normal file
74
dlt-connector/src/controller/TransactionBody.ts
Normal file
@ -0,0 +1,74 @@
|
||||
import { CrossGroupType } from '@/graphql/enum/CrossGroupType'
|
||||
import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType'
|
||||
import { TransactionType } from '@/graphql/enum/TransactionType'
|
||||
import { TransactionDraft } from '@/graphql/input/TransactionDraft'
|
||||
import { TransactionError } from '@/graphql/model/TransactionError'
|
||||
import { GradidoCreation } from '@/proto/3_3/GradidoCreation'
|
||||
import { GradidoTransfer } from '@/proto/3_3/GradidoTransfer'
|
||||
import { TransactionBody } from '@/proto/3_3/TransactionBody'
|
||||
|
||||
export const create = (transaction: TransactionDraft): TransactionBody => {
|
||||
const body = new TransactionBody(transaction)
|
||||
// TODO: load pubkeys for sender and recipient user from db
|
||||
switch (transaction.type) {
|
||||
case TransactionType.CREATION:
|
||||
body.creation = new GradidoCreation(transaction)
|
||||
body.data = 'gradidoCreation'
|
||||
break
|
||||
case TransactionType.SEND:
|
||||
case TransactionType.RECEIVE:
|
||||
body.transfer = new GradidoTransfer(transaction)
|
||||
body.data = 'gradidoTransfer'
|
||||
break
|
||||
}
|
||||
return body
|
||||
}
|
||||
|
||||
export const determineCrossGroupType = ({
|
||||
senderUser,
|
||||
recipientUser,
|
||||
type,
|
||||
}: TransactionDraft): CrossGroupType => {
|
||||
if (
|
||||
!recipientUser.communityUuid ||
|
||||
recipientUser.communityUuid === '' ||
|
||||
senderUser.communityUuid === recipientUser.communityUuid ||
|
||||
type === TransactionType.CREATION
|
||||
) {
|
||||
return CrossGroupType.LOCAL
|
||||
} else if (type === TransactionType.SEND) {
|
||||
return CrossGroupType.INBOUND
|
||||
} else if (type === TransactionType.RECEIVE) {
|
||||
return CrossGroupType.OUTBOUND
|
||||
}
|
||||
throw new TransactionError(
|
||||
TransactionErrorType.NOT_IMPLEMENTED_YET,
|
||||
'cannot determine CrossGroupType',
|
||||
)
|
||||
}
|
||||
|
||||
export const determineOtherGroup = (
|
||||
type: CrossGroupType,
|
||||
{ senderUser, recipientUser }: TransactionDraft,
|
||||
): string => {
|
||||
switch (type) {
|
||||
case CrossGroupType.LOCAL:
|
||||
return ''
|
||||
case CrossGroupType.INBOUND:
|
||||
if (!recipientUser.communityUuid) {
|
||||
throw new TransactionError(
|
||||
TransactionErrorType.MISSING_PARAMETER,
|
||||
'missing recipient user community id for cross group transaction',
|
||||
)
|
||||
}
|
||||
return recipientUser.communityUuid
|
||||
case CrossGroupType.OUTBOUND:
|
||||
if (!senderUser.communityUuid) {
|
||||
throw new TransactionError(
|
||||
TransactionErrorType.MISSING_PARAMETER,
|
||||
'missing sender user community id for cross group transaction',
|
||||
)
|
||||
}
|
||||
return senderUser.communityUuid
|
||||
}
|
||||
}
|
||||
9
dlt-connector/src/graphql/enum/AddressType.ts
Normal file
9
dlt-connector/src/graphql/enum/AddressType.ts
Normal file
@ -0,0 +1,9 @@
|
||||
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
|
||||
}
|
||||
7
dlt-connector/src/graphql/enum/CrossGroupType.ts
Normal file
7
dlt-connector/src/graphql/enum/CrossGroupType.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export enum CrossGroupType {
|
||||
LOCAL = 0,
|
||||
INBOUND = 1,
|
||||
OUTBOUND = 2,
|
||||
// for cross group transaction which haven't a direction like group friend update
|
||||
// CROSS = 3,
|
||||
}
|
||||
13
dlt-connector/src/graphql/enum/TransactionErrorType.ts
Normal file
13
dlt-connector/src/graphql/enum/TransactionErrorType.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { registerEnumType } from 'type-graphql'
|
||||
|
||||
export enum TransactionErrorType {
|
||||
NOT_IMPLEMENTED_YET = 'Not Implemented yet',
|
||||
MISSING_PARAMETER = 'Missing parameter',
|
||||
ALREADY_EXIST = 'Already exist',
|
||||
DB_ERROR = 'DB Error',
|
||||
}
|
||||
|
||||
registerEnumType(TransactionErrorType, {
|
||||
name: 'TransactionErrorType',
|
||||
description: 'Transaction Error Type',
|
||||
})
|
||||
15
dlt-connector/src/graphql/enum/TransactionValidationLevel.ts
Normal file
15
dlt-connector/src/graphql/enum/TransactionValidationLevel.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { registerEnumType } from 'type-graphql'
|
||||
|
||||
export enum TransactionValidationLevel {
|
||||
SINGLE = 1, // check only the transaction
|
||||
SINGLE_PREVIOUS = 2, // check also with previous transaction
|
||||
DATE_RANGE = 3, // check all transaction from within date range by creation automatic the same month
|
||||
PAIRED = 4, // check paired transaction on another group by cross group transactions
|
||||
CONNECTED_GROUP = 5, // check all transactions in the group which connected with this transaction address(es)
|
||||
CONNECTED_BLOCKCHAIN = 6, // check all transactions which connected with this transaction
|
||||
}
|
||||
|
||||
registerEnumType(TransactionValidationLevel, {
|
||||
name: 'TransactionValidationLevel',
|
||||
description: 'Transaction Validation Levels',
|
||||
})
|
||||
20
dlt-connector/src/graphql/input/CommunityDraft.ts
Normal file
20
dlt-connector/src/graphql/input/CommunityDraft.ts
Normal file
@ -0,0 +1,20 @@
|
||||
// https://www.npmjs.com/package/@apollo/protobufjs
|
||||
|
||||
import { IsBoolean, IsUUID } from 'class-validator'
|
||||
import { Field, InputType } from 'type-graphql'
|
||||
import { isValidDateString } from '@validator/DateString'
|
||||
|
||||
@InputType()
|
||||
export class CommunityDraft {
|
||||
@Field(() => String)
|
||||
@IsUUID('4')
|
||||
uuid: string
|
||||
|
||||
@Field(() => Boolean)
|
||||
@IsBoolean()
|
||||
foreign: boolean
|
||||
|
||||
@Field(() => String)
|
||||
@isValidDateString()
|
||||
createdAt: string
|
||||
}
|
||||
39
dlt-connector/src/graphql/input/TransactionDraft.ts
Executable file
39
dlt-connector/src/graphql/input/TransactionDraft.ts
Executable file
@ -0,0 +1,39 @@
|
||||
// https://www.npmjs.com/package/@apollo/protobufjs
|
||||
|
||||
import { Decimal } from 'decimal.js-light'
|
||||
import { TransactionType } from '@enum/TransactionType'
|
||||
import { InputType, Field } from 'type-graphql'
|
||||
import { UserIdentifier } from './UserIdentifier'
|
||||
import { isValidDateString } from '@validator/DateString'
|
||||
import { IsPositiveDecimal } from '@validator/Decimal'
|
||||
import { IsEnum, IsObject, ValidateNested } from 'class-validator'
|
||||
|
||||
@InputType()
|
||||
export class TransactionDraft {
|
||||
@Field(() => UserIdentifier)
|
||||
@IsObject()
|
||||
@ValidateNested()
|
||||
senderUser: UserIdentifier
|
||||
|
||||
@Field(() => UserIdentifier)
|
||||
@IsObject()
|
||||
@ValidateNested()
|
||||
recipientUser: UserIdentifier
|
||||
|
||||
@Field(() => Decimal)
|
||||
@IsPositiveDecimal()
|
||||
amount: Decimal
|
||||
|
||||
@Field(() => TransactionType)
|
||||
@IsEnum(TransactionType)
|
||||
type: TransactionType
|
||||
|
||||
@Field(() => String)
|
||||
@isValidDateString()
|
||||
createdAt: string
|
||||
|
||||
// only for creation transactions
|
||||
@Field(() => String, { nullable: true })
|
||||
@isValidDateString()
|
||||
targetDate?: string
|
||||
}
|
||||
@ -3,16 +3,22 @@
|
||||
import { Decimal } from 'decimal.js-light'
|
||||
import { TransactionType } from '../enum/TransactionType'
|
||||
import { InputType, Field } from 'type-graphql'
|
||||
import { IsEnum, IsInt, Min } from 'class-validator'
|
||||
import { IsPositiveDecimal } from '../validator/Decimal'
|
||||
|
||||
@InputType()
|
||||
export class TransactionInput {
|
||||
@Field(() => TransactionType)
|
||||
@IsEnum(TransactionType)
|
||||
type: TransactionType
|
||||
|
||||
@Field(() => Decimal)
|
||||
@IsPositiveDecimal()
|
||||
amount: Decimal
|
||||
|
||||
@Field(() => Number)
|
||||
@IsInt()
|
||||
@Min(978346800)
|
||||
createdAt: number
|
||||
|
||||
// @protoField.d(4, 'string')
|
||||
|
||||
19
dlt-connector/src/graphql/input/UserIdentifier.ts
Normal file
19
dlt-connector/src/graphql/input/UserIdentifier.ts
Normal file
@ -0,0 +1,19 @@
|
||||
// https://www.npmjs.com/package/@apollo/protobufjs
|
||||
|
||||
import { IsPositive, IsUUID } from 'class-validator'
|
||||
import { Field, Int, InputType } from 'type-graphql'
|
||||
|
||||
@InputType()
|
||||
export class UserIdentifier {
|
||||
@Field(() => String)
|
||||
@IsUUID('4')
|
||||
uuid: string
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
@IsUUID('4')
|
||||
communityUuid?: string
|
||||
|
||||
@Field(() => Int, { defaultValue: 1, nullable: true })
|
||||
@IsPositive()
|
||||
accountNr?: number
|
||||
}
|
||||
20
dlt-connector/src/graphql/model/TransactionError.ts
Normal file
20
dlt-connector/src/graphql/model/TransactionError.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { ObjectType, Field } from 'type-graphql'
|
||||
import { TransactionErrorType } from '../enum/TransactionErrorType'
|
||||
|
||||
@ObjectType()
|
||||
export class TransactionError implements Error {
|
||||
constructor(type: TransactionErrorType, message: string) {
|
||||
this.type = type
|
||||
this.message = message
|
||||
this.name = type.toString()
|
||||
}
|
||||
|
||||
@Field(() => TransactionErrorType)
|
||||
type: TransactionErrorType
|
||||
|
||||
@Field(() => String)
|
||||
message: string
|
||||
|
||||
@Field(() => String)
|
||||
name: string
|
||||
}
|
||||
26
dlt-connector/src/graphql/model/TransactionResult.ts
Normal file
26
dlt-connector/src/graphql/model/TransactionResult.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { ObjectType, Field } from 'type-graphql'
|
||||
import { TransactionError } from './TransactionError'
|
||||
|
||||
@ObjectType()
|
||||
export class TransactionResult {
|
||||
constructor(content?: TransactionError | string) {
|
||||
this.succeed = true
|
||||
if (content instanceof TransactionError) {
|
||||
this.error = content
|
||||
this.succeed = false
|
||||
} else if (typeof content === 'string') {
|
||||
this.messageId = content
|
||||
}
|
||||
}
|
||||
|
||||
// the error if one happened
|
||||
@Field(() => TransactionError, { nullable: true })
|
||||
error?: TransactionError
|
||||
|
||||
// if no error happend, the message id of the iota transaction
|
||||
@Field(() => String, { nullable: true })
|
||||
messageId?: string
|
||||
|
||||
@Field(() => Boolean)
|
||||
succeed: boolean
|
||||
}
|
||||
80
dlt-connector/src/graphql/resolver/CommunityResolver.test.ts
Normal file
80
dlt-connector/src/graphql/resolver/CommunityResolver.test.ts
Normal file
@ -0,0 +1,80 @@
|
||||
import 'reflect-metadata'
|
||||
import { ApolloServer } from '@apollo/server'
|
||||
import { createApolloTestServer } from '@test/ApolloServerMock'
|
||||
import assert from 'assert'
|
||||
import { TestDB } from '@test/TestDB'
|
||||
import { TransactionResult } from '../model/TransactionResult'
|
||||
|
||||
let apolloTestServer: ApolloServer
|
||||
|
||||
jest.mock('@typeorm/DataSource', () => ({
|
||||
getDataSource: () => TestDB.instance.dbConnect,
|
||||
}))
|
||||
|
||||
describe('graphql/resolver/CommunityResolver', () => {
|
||||
beforeAll(async () => {
|
||||
apolloTestServer = await createApolloTestServer()
|
||||
})
|
||||
|
||||
describe('tests with db', () => {
|
||||
beforeAll(async () => {
|
||||
await TestDB.instance.setupTestDB()
|
||||
// apolloTestServer = await createApolloTestServer()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await TestDB.instance.teardownTestDB()
|
||||
})
|
||||
|
||||
it('test add foreign community', async () => {
|
||||
const response = await apolloTestServer.executeOperation({
|
||||
query: 'mutation ($input: CommunityDraft!) { addCommunity(data: $input) {succeed} }',
|
||||
variables: {
|
||||
input: {
|
||||
uuid: '3d813cbb-37fb-42ba-91df-831e1593ac29',
|
||||
foreign: true,
|
||||
createdAt: '2012-04-17T17:12:00.0012Z',
|
||||
},
|
||||
},
|
||||
})
|
||||
assert(response.body.kind === 'single')
|
||||
expect(response.body.singleResult.errors).toBeUndefined()
|
||||
const transactionResult = response.body.singleResult.data?.addCommunity as TransactionResult
|
||||
expect(transactionResult.succeed).toEqual(true)
|
||||
})
|
||||
|
||||
it('test add home community', async () => {
|
||||
const response = await apolloTestServer.executeOperation({
|
||||
query: 'mutation ($input: CommunityDraft!) { addCommunity(data: $input) {succeed} }',
|
||||
variables: {
|
||||
input: {
|
||||
uuid: '3d823cad-37fb-41cd-91df-152e1593ac29',
|
||||
foreign: false,
|
||||
createdAt: '2012-05-12T13:12:00.2917Z',
|
||||
},
|
||||
},
|
||||
})
|
||||
assert(response.body.kind === 'single')
|
||||
expect(response.body.singleResult.errors).toBeUndefined()
|
||||
const transactionResult = response.body.singleResult.data?.addCommunity as TransactionResult
|
||||
expect(transactionResult.succeed).toEqual(true)
|
||||
})
|
||||
|
||||
it('test add existing community', async () => {
|
||||
const response = await apolloTestServer.executeOperation({
|
||||
query: 'mutation ($input: CommunityDraft!) { addCommunity(data: $input) {succeed} }',
|
||||
variables: {
|
||||
input: {
|
||||
uuid: '3d823cad-37fb-41cd-91df-152e1593ac29',
|
||||
foreign: false,
|
||||
createdAt: '2012-05-12T13:12:00.1271Z',
|
||||
},
|
||||
},
|
||||
})
|
||||
assert(response.body.kind === 'single')
|
||||
expect(response.body.singleResult.errors).toBeUndefined()
|
||||
const transactionResult = response.body.singleResult.data?.addCommunity as TransactionResult
|
||||
expect(transactionResult.succeed).toEqual(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
53
dlt-connector/src/graphql/resolver/CommunityResolver.ts
Normal file
53
dlt-connector/src/graphql/resolver/CommunityResolver.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import { Resolver, Arg, Mutation } from 'type-graphql'
|
||||
|
||||
import { CommunityDraft } from '@input/CommunityDraft'
|
||||
|
||||
import { TransactionResult } from '../model/TransactionResult'
|
||||
import { TransactionError } from '../model/TransactionError'
|
||||
import { create as createCommunity, isExist } from '@/controller/Community'
|
||||
import { TransactionErrorType } from '../enum/TransactionErrorType'
|
||||
import { logger } from '@/server/logger'
|
||||
import { iotaTopicFromCommunityUUID } from '@/utils/typeConverter'
|
||||
|
||||
@Resolver()
|
||||
export class CommunityResolver {
|
||||
@Mutation(() => TransactionResult)
|
||||
async addCommunity(
|
||||
@Arg('data')
|
||||
communityDraft: CommunityDraft,
|
||||
): Promise<TransactionResult> {
|
||||
try {
|
||||
const topic = iotaTopicFromCommunityUUID(communityDraft.uuid)
|
||||
|
||||
// check if community was already written to db
|
||||
if (await isExist(topic)) {
|
||||
return new TransactionResult(
|
||||
new TransactionError(TransactionErrorType.ALREADY_EXIST, 'community already exist!'),
|
||||
)
|
||||
}
|
||||
const community = createCommunity(communityDraft, topic)
|
||||
|
||||
let result: TransactionResult
|
||||
|
||||
if (!communityDraft.foreign) {
|
||||
// TODO: CommunityRoot Transaction for blockchain
|
||||
}
|
||||
try {
|
||||
await community.save()
|
||||
result = new TransactionResult()
|
||||
} catch (err) {
|
||||
logger.error('error saving new community into db: %s', err)
|
||||
result = new TransactionResult(
|
||||
new TransactionError(TransactionErrorType.DB_ERROR, 'error saving community into db'),
|
||||
)
|
||||
}
|
||||
return result
|
||||
} catch (error) {
|
||||
if (error instanceof TransactionError) {
|
||||
return new TransactionResult(error)
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2,6 +2,9 @@ import 'reflect-metadata'
|
||||
import { ApolloServer } from '@apollo/server'
|
||||
import { createApolloTestServer } from '@test/ApolloServerMock'
|
||||
import assert from 'assert'
|
||||
import { TransactionResult } from '../model/TransactionResult'
|
||||
|
||||
let apolloTestServer: ApolloServer
|
||||
|
||||
jest.mock('@/client/IotaClient', () => {
|
||||
return {
|
||||
@ -11,8 +14,6 @@ jest.mock('@/client/IotaClient', () => {
|
||||
}
|
||||
})
|
||||
|
||||
let apolloTestServer: ApolloServer
|
||||
|
||||
describe('Transaction Resolver Test', () => {
|
||||
beforeAll(async () => {
|
||||
apolloTestServer = await createApolloTestServer()
|
||||
@ -31,30 +32,45 @@ describe('Transaction Resolver Test', () => {
|
||||
})
|
||||
it('test mocked sendTransaction', async () => {
|
||||
const response = await apolloTestServer.executeOperation({
|
||||
query: 'mutation ($input: TransactionInput!) { sendTransaction(data: $input) }',
|
||||
query:
|
||||
'mutation ($input: TransactionDraft!) { sendTransaction(data: $input) {error {type, message}, messageId} }',
|
||||
variables: {
|
||||
input: {
|
||||
senderUser: {
|
||||
uuid: '0ec72b74-48c2-446f-91ce-31ad7d9f4d65',
|
||||
},
|
||||
recipientUser: {
|
||||
uuid: 'ddc8258e-fcb5-4e48-8d1d-3a07ec371dbe',
|
||||
},
|
||||
type: 'SEND',
|
||||
amount: '10',
|
||||
createdAt: 1688992436,
|
||||
createdAt: '2012-04-17T17:12:00Z',
|
||||
},
|
||||
},
|
||||
})
|
||||
assert(response.body.kind === 'single')
|
||||
expect(response.body.singleResult.errors).toBeUndefined()
|
||||
expect(response.body.singleResult.data?.sendTransaction).toBe(
|
||||
const transactionResult = response.body.singleResult.data?.sendTransaction as TransactionResult
|
||||
expect(transactionResult.messageId).toBe(
|
||||
'5498130bc3918e1a7143969ce05805502417e3e1bd596d3c44d6a0adeea22710',
|
||||
)
|
||||
})
|
||||
|
||||
it('test mocked sendTransaction invalid transactionType ', async () => {
|
||||
const response = await apolloTestServer.executeOperation({
|
||||
query: 'mutation ($input: TransactionInput!) { sendTransaction(data: $input) }',
|
||||
query:
|
||||
'mutation ($input: TransactionDraft!) { sendTransaction(data: $input) {error {type, message}, messageId} }',
|
||||
variables: {
|
||||
input: {
|
||||
senderUser: {
|
||||
uuid: '0ec72b74-48c2-446f-91ce-31ad7d9f4d65',
|
||||
},
|
||||
recipientUser: {
|
||||
uuid: 'ddc8258e-fcb5-4e48-8d1d-3a07ec371dbe',
|
||||
},
|
||||
type: 'INVALID',
|
||||
amount: '10',
|
||||
createdAt: 1688992436,
|
||||
createdAt: '2012-04-17T17:12:00Z',
|
||||
},
|
||||
},
|
||||
})
|
||||
@ -71,12 +87,19 @@ describe('Transaction Resolver Test', () => {
|
||||
|
||||
it('test mocked sendTransaction invalid amount ', async () => {
|
||||
const response = await apolloTestServer.executeOperation({
|
||||
query: 'mutation ($input: TransactionInput!) { sendTransaction(data: $input) }',
|
||||
query:
|
||||
'mutation ($input: TransactionDraft!) { sendTransaction(data: $input) {error {type, message}, messageId} }',
|
||||
variables: {
|
||||
input: {
|
||||
senderUser: {
|
||||
uuid: '0ec72b74-48c2-446f-91ce-31ad7d9f4d65',
|
||||
},
|
||||
recipientUser: {
|
||||
uuid: 'ddc8258e-fcb5-4e48-8d1d-3a07ec371dbe',
|
||||
},
|
||||
type: 'SEND',
|
||||
amount: 'no number',
|
||||
createdAt: 1688992436,
|
||||
createdAt: '2012-04-17T17:12:00Z',
|
||||
},
|
||||
},
|
||||
})
|
||||
@ -93,12 +116,19 @@ describe('Transaction Resolver Test', () => {
|
||||
|
||||
it('test mocked sendTransaction invalid created date ', async () => {
|
||||
const response = await apolloTestServer.executeOperation({
|
||||
query: 'mutation ($input: TransactionInput!) { sendTransaction(data: $input) }',
|
||||
query:
|
||||
'mutation ($input: TransactionDraft!) { sendTransaction(data: $input) {error {type, message}, messageId} }',
|
||||
variables: {
|
||||
input: {
|
||||
senderUser: {
|
||||
uuid: '0ec72b74-48c2-446f-91ce-31ad7d9f4d65',
|
||||
},
|
||||
recipientUser: {
|
||||
uuid: 'ddc8258e-fcb5-4e48-8d1d-3a07ec371dbe',
|
||||
},
|
||||
type: 'SEND',
|
||||
amount: '10',
|
||||
createdAt: '2023-03-02T10:12:00',
|
||||
createdAt: 'not valid',
|
||||
},
|
||||
},
|
||||
})
|
||||
@ -106,10 +136,47 @@ describe('Transaction Resolver Test', () => {
|
||||
expect(response.body.singleResult).toMatchObject({
|
||||
errors: [
|
||||
{
|
||||
message:
|
||||
'Variable "$input" got invalid value "2023-03-02T10:12:00" at "input.createdAt"; Float cannot represent non numeric value: "2023-03-02T10:12:00"',
|
||||
message: 'Argument Validation Error',
|
||||
extensions: {
|
||||
code: 'BAD_USER_INPUT',
|
||||
validationErrors: [
|
||||
{
|
||||
value: 'not valid',
|
||||
property: 'createdAt',
|
||||
constraints: {
|
||||
isValidDateString: 'createdAt must be a valid date string',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
it('test mocked sendTransaction missing creationDate for contribution', async () => {
|
||||
const response = await apolloTestServer.executeOperation({
|
||||
query:
|
||||
'mutation ($input: TransactionDraft!) { sendTransaction(data: $input) {error {type, message}, messageId} }',
|
||||
variables: {
|
||||
input: {
|
||||
senderUser: {
|
||||
uuid: '0ec72b74-48c2-446f-91ce-31ad7d9f4d65',
|
||||
},
|
||||
recipientUser: {
|
||||
uuid: 'ddc8258e-fcb5-4e48-8d1d-3a07ec371dbe',
|
||||
},
|
||||
type: 'CREATION',
|
||||
amount: '10',
|
||||
createdAt: '2012-04-17T17:12:00Z',
|
||||
},
|
||||
},
|
||||
})
|
||||
assert(response.body.kind === 'single')
|
||||
expect(response.body.singleResult.data?.sendTransaction).toMatchObject({
|
||||
error: {
|
||||
type: 'MISSING_PARAMETER',
|
||||
message: 'missing targetDate for contribution',
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,9 +1,14 @@
|
||||
import { Resolver, Query, Arg, Mutation } from 'type-graphql'
|
||||
|
||||
import { TransactionInput } from '@input/TransactionInput'
|
||||
import { TransactionBody } from '@proto/TransactionBody'
|
||||
import { TransactionDraft } from '@input/TransactionDraft'
|
||||
|
||||
import { create as createTransactionBody } from '@controller/TransactionBody'
|
||||
import { create as createGradidoTransaction } from '@controller/GradidoTransaction'
|
||||
|
||||
import { sendMessage as iotaSendMessage } from '@/client/IotaClient'
|
||||
import { GradidoTransaction } from '@/proto/3_3/GradidoTransaction'
|
||||
import { TransactionResult } from '../model/TransactionResult'
|
||||
import { TransactionError } from '../model/TransactionError'
|
||||
|
||||
@Resolver()
|
||||
export class TransactionResolver {
|
||||
@ -18,14 +23,23 @@ export class TransactionResolver {
|
||||
return '0.1'
|
||||
}
|
||||
|
||||
@Mutation(() => String)
|
||||
@Mutation(() => TransactionResult)
|
||||
async sendTransaction(
|
||||
@Arg('data')
|
||||
transaction: TransactionInput,
|
||||
): Promise<string> {
|
||||
const message = TransactionBody.fromObject(transaction)
|
||||
const messageBuffer = TransactionBody.encode(message).finish()
|
||||
const resultMessage = await iotaSendMessage(messageBuffer)
|
||||
return resultMessage.messageId
|
||||
transaction: TransactionDraft,
|
||||
): Promise<TransactionResult> {
|
||||
try {
|
||||
const body = createTransactionBody(transaction)
|
||||
const message = createGradidoTransaction(body)
|
||||
const messageBuffer = GradidoTransaction.encode(message).finish()
|
||||
const resultMessage = await iotaSendMessage(messageBuffer)
|
||||
return new TransactionResult(resultMessage.messageId)
|
||||
} catch (error) {
|
||||
if (error instanceof TransactionError) {
|
||||
return new TransactionResult(error)
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
import { Decimal } from 'decimal.js-light'
|
||||
import { GraphQLScalarType, Kind, ValueNode } from 'graphql'
|
||||
|
||||
export const DecimalScalar = new GraphQLScalarType<Decimal, string>({
|
||||
export const DecimalScalar = new GraphQLScalarType({
|
||||
name: 'Decimal',
|
||||
description: 'The `Decimal` scalar type to represent currency values',
|
||||
|
||||
|
||||
@ -4,10 +4,19 @@ import { buildSchema } from 'type-graphql'
|
||||
|
||||
import { DecimalScalar } from './scalar/Decimal'
|
||||
import { TransactionResolver } from './resolver/TransactionsResolver'
|
||||
import { CommunityResolver } from './resolver/CommunityResolver'
|
||||
|
||||
export const schema = async (): Promise<GraphQLSchema> => {
|
||||
return buildSchema({
|
||||
resolvers: [TransactionResolver],
|
||||
resolvers: [TransactionResolver, CommunityResolver],
|
||||
scalarsMap: [{ type: Decimal, scalar: DecimalScalar }],
|
||||
validate: {
|
||||
validationError: { target: false },
|
||||
skipMissingProperties: true,
|
||||
skipNullProperties: true,
|
||||
skipUndefinedProperties: false,
|
||||
forbidUnknownValues: true,
|
||||
stopAtFirstError: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
21
dlt-connector/src/graphql/validator/DateString.ts
Normal file
21
dlt-connector/src/graphql/validator/DateString.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { registerDecorator, ValidationOptions } from 'class-validator'
|
||||
|
||||
export function isValidDateString(validationOptions?: ValidationOptions) {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
return function (object: Object, propertyName: string) {
|
||||
registerDecorator({
|
||||
name: 'isValidDateString',
|
||||
target: object.constructor,
|
||||
propertyName,
|
||||
options: validationOptions,
|
||||
validator: {
|
||||
validate(value: string): boolean {
|
||||
return !isNaN(Date.parse(value))
|
||||
},
|
||||
defaultMessage(): string {
|
||||
return `${propertyName} must be a valid date string`
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
22
dlt-connector/src/graphql/validator/Decimal.ts
Normal file
22
dlt-connector/src/graphql/validator/Decimal.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { registerDecorator, ValidationOptions, ValidationArguments } from 'class-validator'
|
||||
import { Decimal } from 'decimal.js-light'
|
||||
|
||||
export function IsPositiveDecimal(validationOptions?: ValidationOptions) {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
return function (object: Object, propertyName: string) {
|
||||
registerDecorator({
|
||||
name: 'isPositiveDecimal',
|
||||
target: object.constructor,
|
||||
propertyName,
|
||||
options: validationOptions,
|
||||
validator: {
|
||||
validate(value: Decimal): boolean {
|
||||
return value.greaterThan(0)
|
||||
},
|
||||
defaultMessage(args: ValidationArguments): string {
|
||||
return `The ${propertyName} must be a positive value ${args.property}`
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
34
dlt-connector/src/proto/3_3/GradidoConfirmedTransaction.ts
Normal file
34
dlt-connector/src/proto/3_3/GradidoConfirmedTransaction.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import { Field, Message } from '@apollo/protobufjs'
|
||||
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 GradidoConfirmedTransaction extends Message<GradidoConfirmedTransaction> {
|
||||
@Field.d(1, 'uint64')
|
||||
id: number
|
||||
|
||||
@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
|
||||
}
|
||||
20
dlt-connector/src/proto/3_3/GradidoCreation.test.ts
Normal file
20
dlt-connector/src/proto/3_3/GradidoCreation.test.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import 'reflect-metadata'
|
||||
import { TransactionDraft } from '@/graphql/input/TransactionDraft'
|
||||
import { GradidoCreation } from './GradidoCreation'
|
||||
import { TransactionError } from '@/graphql/model/TransactionError'
|
||||
import { TransactionErrorType } from '@enum/TransactionErrorType'
|
||||
|
||||
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',
|
||||
),
|
||||
)
|
||||
})
|
||||
})
|
||||
32
dlt-connector/src/proto/3_3/GradidoCreation.ts
Normal file
32
dlt-connector/src/proto/3_3/GradidoCreation.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { Field, Message } from '@apollo/protobufjs'
|
||||
|
||||
import { TimestampSeconds } from './TimestampSeconds'
|
||||
import { TransferAmount } from './TransferAmount'
|
||||
import { TransactionDraft } from '@/graphql/input/TransactionDraft'
|
||||
import { TransactionError } from '@/graphql/model/TransactionError'
|
||||
import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType'
|
||||
|
||||
// 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> {
|
||||
constructor(transaction: TransactionDraft) {
|
||||
if (!transaction.targetDate) {
|
||||
throw new TransactionError(
|
||||
TransactionErrorType.MISSING_PARAMETER,
|
||||
'missing targetDate for contribution',
|
||||
)
|
||||
}
|
||||
super({
|
||||
recipient: new TransferAmount({ amount: transaction.amount.toString() }),
|
||||
targetDate: new TimestampSeconds(new Date(transaction.targetDate)),
|
||||
})
|
||||
}
|
||||
|
||||
@Field.d(1, TransferAmount)
|
||||
public recipient: TransferAmount
|
||||
|
||||
@Field.d(3, 'TimestampSeconds')
|
||||
public targetDate: TimestampSeconds
|
||||
}
|
||||
31
dlt-connector/src/proto/3_3/GradidoDeferredTransfer.ts
Normal file
31
dlt-connector/src/proto/3_3/GradidoDeferredTransfer.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { Field, Message } from '@apollo/protobufjs'
|
||||
|
||||
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
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
export class GradidoDeferredTransfer extends Message<GradidoDeferredTransfer> {
|
||||
// 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?
|
||||
}
|
||||
21
dlt-connector/src/proto/3_3/GradidoTransaction.ts
Normal file
21
dlt-connector/src/proto/3_3/GradidoTransaction.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { Field, Message } from '@apollo/protobufjs'
|
||||
|
||||
import { SignatureMap } from './SignatureMap'
|
||||
|
||||
// https://www.npmjs.com/package/@apollo/protobufjs
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
export class GradidoTransaction extends Message<GradidoTransaction> {
|
||||
@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
|
||||
}
|
||||
23
dlt-connector/src/proto/3_3/GradidoTransfer.ts
Normal file
23
dlt-connector/src/proto/3_3/GradidoTransfer.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { Field, Message } from '@apollo/protobufjs'
|
||||
|
||||
import { TransferAmount } from './TransferAmount'
|
||||
import { TransactionDraft } from '@/graphql/input/TransactionDraft'
|
||||
|
||||
// https://www.npmjs.com/package/@apollo/protobufjs
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
export class GradidoTransfer extends Message<GradidoTransfer> {
|
||||
constructor(transaction: TransactionDraft, coinOrigin?: string) {
|
||||
super({
|
||||
sender: new TransferAmount({
|
||||
amount: transaction.amount.toString(),
|
||||
communityId: coinOrigin,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
@Field.d(1, TransferAmount)
|
||||
public sender: TransferAmount
|
||||
|
||||
@Field.d(2, 'bytes')
|
||||
public recipient: Buffer
|
||||
}
|
||||
15
dlt-connector/src/proto/3_3/GroupFriendsUpdate.ts
Normal file
15
dlt-connector/src/proto/3_3/GroupFriendsUpdate.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { Field, Message } from '@apollo/protobufjs'
|
||||
|
||||
// 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> {
|
||||
// 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
|
||||
}
|
||||
19
dlt-connector/src/proto/3_3/RegisterAddress.ts
Normal file
19
dlt-connector/src/proto/3_3/RegisterAddress.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { Field, Message } from '@apollo/protobufjs'
|
||||
|
||||
import { AddressType } from '@enum/AddressType'
|
||||
|
||||
// https://www.npmjs.com/package/@apollo/protobufjs
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
export class RegisterAddress extends Message<RegisterAddress> {
|
||||
@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 subaccountPubkey: Buffer
|
||||
}
|
||||
10
dlt-connector/src/proto/3_3/SignatureMap.ts
Normal file
10
dlt-connector/src/proto/3_3/SignatureMap.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { Field, Message } from '@apollo/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> {
|
||||
@Field.d(1, SignaturePair, 'repeated')
|
||||
public sigPair: SignaturePair
|
||||
}
|
||||
11
dlt-connector/src/proto/3_3/SignaturePair.ts
Normal file
11
dlt-connector/src/proto/3_3/SignaturePair.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { Field, Message } from '@apollo/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
|
||||
}
|
||||
16
dlt-connector/src/proto/3_3/Timestamp.test.ts
Normal file
16
dlt-connector/src/proto/3_3/Timestamp.test.ts
Normal file
@ -0,0 +1,16 @@
|
||||
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)
|
||||
})
|
||||
})
|
||||
27
dlt-connector/src/proto/3_3/Timestamp.ts
Normal file
27
dlt-connector/src/proto/3_3/Timestamp.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { Field, Message } from '@apollo/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
|
||||
}
|
||||
14
dlt-connector/src/proto/3_3/TimestampSeconds.test.ts
Normal file
14
dlt-connector/src/proto/3_3/TimestampSeconds.test.ts
Normal file
@ -0,0 +1,14 @@
|
||||
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)
|
||||
})
|
||||
})
|
||||
20
dlt-connector/src/proto/3_3/TimestampSeconds.ts
Normal file
20
dlt-connector/src/proto/3_3/TimestampSeconds.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { Field, Message } from '@apollo/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
|
||||
}
|
||||
66
dlt-connector/src/proto/3_3/TransactionBody.ts
Normal file
66
dlt-connector/src/proto/3_3/TransactionBody.ts
Normal file
@ -0,0 +1,66 @@
|
||||
import { Field, Message, OneOf } from '@apollo/protobufjs'
|
||||
|
||||
import { CrossGroupType } from '@/graphql/enum/CrossGroupType'
|
||||
|
||||
import { Timestamp } from './Timestamp'
|
||||
import { GradidoTransfer } from './GradidoTransfer'
|
||||
import { GradidoCreation } from './GradidoCreation'
|
||||
import { GradidoDeferredTransfer } from './GradidoDeferredTransfer'
|
||||
import { GroupFriendsUpdate } from './GroupFriendsUpdate'
|
||||
import { RegisterAddress } from './RegisterAddress'
|
||||
import { TransactionDraft } from '@/graphql/input/TransactionDraft'
|
||||
import { determineCrossGroupType, determineOtherGroup } from '@/controller/TransactionBody'
|
||||
|
||||
// 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) {
|
||||
const type = determineCrossGroupType(transaction)
|
||||
super({
|
||||
memo: 'Not implemented yet',
|
||||
createdAt: new Timestamp(new Date(transaction.createdAt)),
|
||||
versionNumber: '3.3',
|
||||
type,
|
||||
otherGroup: determineOtherGroup(type, 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',
|
||||
)
|
||||
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
|
||||
}
|
||||
16
dlt-connector/src/proto/3_3/TransferAmount.ts
Normal file
16
dlt-connector/src/proto/3_3/TransferAmount.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { Field, Message } from '@apollo/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,36 +0,0 @@
|
||||
import 'reflect-metadata'
|
||||
import { TransactionType } from '@enum/TransactionType'
|
||||
import { TransactionInput } from '@input/TransactionInput'
|
||||
import Decimal from 'decimal.js-light'
|
||||
import { TransactionBody } from './TransactionBody'
|
||||
|
||||
describe('proto/TransactionBodyTest', () => {
|
||||
it('test compatible with graphql/input/TransactionInput', async () => {
|
||||
// test data
|
||||
const type = TransactionType.SEND
|
||||
const amount = new Decimal('10')
|
||||
const createdAt = 1688992436
|
||||
|
||||
// init both objects
|
||||
// graphql input object
|
||||
const transactionInput = new TransactionInput()
|
||||
transactionInput.type = type
|
||||
transactionInput.amount = amount
|
||||
transactionInput.createdAt = createdAt
|
||||
|
||||
// protobuf object
|
||||
const transactionBody = new TransactionBody()
|
||||
transactionBody.type = type
|
||||
transactionBody.amount = amount.toString()
|
||||
transactionBody.createdAt = createdAt
|
||||
|
||||
// create protobuf object from graphql Input object
|
||||
const message = TransactionBody.fromObject(transactionInput)
|
||||
// serialize both protobuf objects
|
||||
const messageBuffer = TransactionBody.encode(message).finish()
|
||||
const messageBuffer2 = TransactionBody.encode(transactionBody).finish()
|
||||
|
||||
// compare
|
||||
expect(messageBuffer).toStrictEqual(messageBuffer2)
|
||||
})
|
||||
})
|
||||
@ -1,18 +0,0 @@
|
||||
import { TransactionType } from '../graphql/enum/TransactionType'
|
||||
import { Field, Message } from '@apollo/protobufjs'
|
||||
|
||||
// https://www.npmjs.com/package/@apollo/protobufjs
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
export class TransactionBody extends Message<TransactionBody> {
|
||||
@Field.d(1, TransactionType)
|
||||
type: TransactionType
|
||||
|
||||
@Field.d(2, 'string')
|
||||
amount: string
|
||||
|
||||
@Field.d(3, 'uint64')
|
||||
createdAt: number
|
||||
|
||||
// @protoField.d(4, 'string')
|
||||
// communitySum: Decimal
|
||||
}
|
||||
28
dlt-connector/src/typeorm/DBVersion.ts
Normal file
28
dlt-connector/src/typeorm/DBVersion.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { Migration } from '@entity/Migration'
|
||||
|
||||
import { logger } from '@/server/logger'
|
||||
|
||||
const getDBVersion = async (): Promise<string | null> => {
|
||||
try {
|
||||
const [dbVersion] = await Migration.find({ order: { version: 'DESC' }, take: 1 })
|
||||
return dbVersion ? dbVersion.fileName : null
|
||||
} catch (error) {
|
||||
logger.error(error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const checkDBVersion = async (DB_VERSION: string): Promise<boolean> => {
|
||||
const dbVersion = await getDBVersion()
|
||||
if (!dbVersion?.includes(DB_VERSION)) {
|
||||
logger.error(
|
||||
`Wrong database version detected - the backend requires '${DB_VERSION}' but found '${
|
||||
dbVersion ?? 'None'
|
||||
}`,
|
||||
)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
export { checkDBVersion, getDBVersion }
|
||||
26
dlt-connector/src/typeorm/DataSource.ts
Normal file
26
dlt-connector/src/typeorm/DataSource.ts
Normal file
@ -0,0 +1,26 @@
|
||||
// TODO This is super weird - since the entities are defined in another project they have their own globals.
|
||||
// We cannot use our connection here, but must use the external typeorm installation
|
||||
import { DataSource as DBDataSource, FileLogger } from '@dbTools/typeorm'
|
||||
import { entities } from '@entity/index'
|
||||
|
||||
import { CONFIG } from '@/config'
|
||||
|
||||
const DataSource = new DBDataSource({
|
||||
type: 'mysql',
|
||||
host: CONFIG.DB_HOST,
|
||||
port: CONFIG.DB_PORT,
|
||||
username: CONFIG.DB_USER,
|
||||
password: CONFIG.DB_PASSWORD,
|
||||
database: CONFIG.DB_DATABASE,
|
||||
entities,
|
||||
synchronize: false,
|
||||
logging: true,
|
||||
logger: new FileLogger('all', {
|
||||
logPath: CONFIG.TYPEORM_LOGGING_RELATIVE_PATH,
|
||||
}),
|
||||
extra: {
|
||||
charset: 'utf8mb4_unicode_ci',
|
||||
},
|
||||
})
|
||||
|
||||
export const getDataSource = () => DataSource
|
||||
17
dlt-connector/src/utils/typeConverter.ts
Normal file
17
dlt-connector/src/utils/typeConverter.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { crypto_generichash as cryptoHash } from 'sodium-native'
|
||||
|
||||
export const uuid4ToBuffer = (uuid: string): Buffer => {
|
||||
// Remove dashes from the UUIDv4 string
|
||||
const cleanedUUID = uuid.replace(/-/g, '')
|
||||
|
||||
// Create a Buffer object from the hexadecimal values
|
||||
const buffer = Buffer.from(cleanedUUID, 'hex')
|
||||
|
||||
return buffer
|
||||
}
|
||||
|
||||
export const iotaTopicFromCommunityUUID = (communityUUID: string): string => {
|
||||
const hash = Buffer.alloc(32)
|
||||
cryptoHash(hash, uuid4ToBuffer(communityUUID))
|
||||
return hash.toString('hex')
|
||||
}
|
||||
77
dlt-connector/test/TestDB.ts
Normal file
77
dlt-connector/test/TestDB.ts
Normal file
@ -0,0 +1,77 @@
|
||||
import { DataSource, FileLogger } from '@dbTools/typeorm'
|
||||
import { createDatabase } from 'typeorm-extension'
|
||||
|
||||
import { entities } from '@entity/index'
|
||||
|
||||
import { CONFIG } from '@/config'
|
||||
import { LogError } from '@/server/LogError'
|
||||
|
||||
export class TestDB {
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
private static _instance: TestDB
|
||||
|
||||
private constructor() {
|
||||
if (!CONFIG.DB_DATABASE_TEST) {
|
||||
throw new LogError('no test db in config')
|
||||
}
|
||||
if (CONFIG.DB_DATABASE === CONFIG.DB_DATABASE_TEST) {
|
||||
throw new LogError(
|
||||
'main db is the same as test db, not good because test db will be cleared after each test run',
|
||||
)
|
||||
}
|
||||
this.dbConnect = new DataSource({
|
||||
type: 'mysql',
|
||||
host: CONFIG.DB_HOST,
|
||||
port: CONFIG.DB_PORT,
|
||||
username: CONFIG.DB_USER,
|
||||
password: CONFIG.DB_PASSWORD,
|
||||
database: CONFIG.DB_DATABASE_TEST,
|
||||
entities,
|
||||
synchronize: true,
|
||||
dropSchema: true,
|
||||
logging: true,
|
||||
logger: new FileLogger('all', {
|
||||
logPath: CONFIG.TYPEORM_LOGGING_RELATIVE_PATH,
|
||||
}),
|
||||
extra: {
|
||||
charset: 'utf8mb4_unicode_ci',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
public static get instance(): TestDB {
|
||||
if (!this._instance) this._instance = new TestDB()
|
||||
return this._instance
|
||||
}
|
||||
|
||||
public dbConnect!: DataSource
|
||||
|
||||
async setupTestDB() {
|
||||
// eslint-disable-next-line no-console
|
||||
try {
|
||||
if (!CONFIG.DB_DATABASE_TEST) {
|
||||
throw new LogError('no test db in config')
|
||||
}
|
||||
await createDatabase({
|
||||
ifNotExist: true,
|
||||
options: {
|
||||
type: 'mysql',
|
||||
charset: 'utf8mb4_unicode_ci',
|
||||
host: CONFIG.DB_HOST,
|
||||
port: CONFIG.DB_PORT,
|
||||
username: CONFIG.DB_USER,
|
||||
password: CONFIG.DB_PASSWORD,
|
||||
database: CONFIG.DB_DATABASE_TEST,
|
||||
},
|
||||
})
|
||||
await this.dbConnect.initialize()
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
async teardownTestDB() {
|
||||
await this.dbConnect.destroy()
|
||||
}
|
||||
}
|
||||
@ -51,11 +51,17 @@
|
||||
"@arg/*": ["src/graphql/arg/*"],
|
||||
"@enum/*": ["src/graphql/enum/*"],
|
||||
"@input/*": ["src/graphql/input/*"],
|
||||
"@model/*": ["src/graphql/model/*"],
|
||||
"@resolver/*": ["src/graphql/resolver/*"],
|
||||
"@scalar/*": ["src/graphql/scalar/*"],
|
||||
"@test/*": ["test/*"],
|
||||
"@proto/*" : ["src/proto/*"],
|
||||
"@controller/*": ["src/controller/*"],
|
||||
"@validator/*" : ["src/graphql/validator/*"],
|
||||
"@typeorm/*" : ["src/typeorm/*"],
|
||||
/* external */
|
||||
"@dbTools/*": ["../dlt-database/src/*", "../../dlt-database/build/src/*"],
|
||||
"@entity/*": ["../dlt-database/entity/*", "../../dlt-database/build/entity/*"]
|
||||
},
|
||||
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
|
||||
"typeRoots": ["node_modules/@types"], /* List of folders to include type definitions from. */
|
||||
@ -79,4 +85,11 @@
|
||||
"skipLibCheck": true, /* Skip type checking of declaration files. */
|
||||
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
|
||||
},
|
||||
"references": [
|
||||
{
|
||||
"path": "../dlt-database/tsconfig.json",
|
||||
// add 'prepend' if you want to include the referenced project in your output file
|
||||
// "prepend": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
6
dlt-database/.env.dist
Normal file
6
dlt-database/.env.dist
Normal file
@ -0,0 +1,6 @@
|
||||
DB_HOST=localhost
|
||||
DB_PORT=3306
|
||||
DB_USER=root
|
||||
DB_PASSWORD=
|
||||
DB_DATABASE=gradido_dlt
|
||||
MIGRATIONS_TABLE=migrations
|
||||
8
dlt-database/.env.template
Normal file
8
dlt-database/.env.template
Normal file
@ -0,0 +1,8 @@
|
||||
CONFIG_VERSION=$DATABASE_CONFIG_VERSION
|
||||
|
||||
DB_HOST=localhost
|
||||
DB_PORT=3306
|
||||
DB_USER=$DB_USER
|
||||
DB_PASSWORD=$DB_PASSWORD
|
||||
DB_DATABASE=gradido_dlt
|
||||
MIGRATIONS_TABLE=migrations
|
||||
3
dlt-database/.eslintignore
Normal file
3
dlt-database/.eslintignore
Normal file
@ -0,0 +1,3 @@
|
||||
node_modules
|
||||
**/*.min.js
|
||||
build
|
||||
206
dlt-database/.eslintrc.js
Normal file
206
dlt-database/.eslintrc.js
Normal file
@ -0,0 +1,206 @@
|
||||
// eslint-disable-next-line import/no-commonjs, import/unambiguous
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
node: true,
|
||||
},
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: ['prettier', '@typescript-eslint', 'import', 'n', 'promise'],
|
||||
extends: [
|
||||
'standard',
|
||||
'eslint:recommended',
|
||||
'plugin:prettier/recommended',
|
||||
'plugin:import/recommended',
|
||||
'plugin:import/typescript',
|
||||
// 'plugin:security/recommended',
|
||||
'plugin:@eslint-community/eslint-comments/recommended',
|
||||
],
|
||||
settings: {
|
||||
'import/parsers': {
|
||||
'@typescript-eslint/parser': ['.ts', '.tsx'],
|
||||
},
|
||||
'import/resolver': {
|
||||
typescript: {
|
||||
project: ['./tsconfig.json'],
|
||||
},
|
||||
node: true,
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'no-console': 'error',
|
||||
camelcase: 'error',
|
||||
'no-debugger': 'error',
|
||||
'prettier/prettier': [
|
||||
'error',
|
||||
{
|
||||
htmlWhitespaceSensitivity: 'ignore',
|
||||
},
|
||||
],
|
||||
// import
|
||||
'import/export': 'error',
|
||||
'import/no-deprecated': 'error',
|
||||
'import/no-empty-named-blocks': 'error',
|
||||
// 'import/no-extraneous-dependencies': 'error',
|
||||
'import/no-mutable-exports': 'error',
|
||||
'import/no-unused-modules': 'error',
|
||||
'import/no-named-as-default': 'error',
|
||||
'import/no-named-as-default-member': 'error',
|
||||
'import/no-amd': 'error',
|
||||
'import/no-commonjs': 'error',
|
||||
'import/no-import-module-exports': 'error',
|
||||
'import/no-nodejs-modules': 'off',
|
||||
'import/unambiguous': 'error',
|
||||
'import/default': 'error',
|
||||
'import/named': 'error',
|
||||
'import/namespace': 'error',
|
||||
'import/no-absolute-path': 'error',
|
||||
// 'import/no-cycle': 'error',
|
||||
'import/no-dynamic-require': 'error',
|
||||
'import/no-internal-modules': 'off',
|
||||
'import/no-relative-packages': 'error',
|
||||
// 'import/no-relative-parent-imports': ['error', { ignore: ['@/*'] }],
|
||||
'import/no-self-import': 'error',
|
||||
'import/no-unresolved': 'error',
|
||||
'import/no-useless-path-segments': 'error',
|
||||
'import/no-webpack-loader-syntax': 'error',
|
||||
'import/consistent-type-specifier-style': 'error',
|
||||
'import/exports-last': 'off',
|
||||
'import/extensions': 'error',
|
||||
'import/first': 'error',
|
||||
'import/group-exports': 'off',
|
||||
'import/newline-after-import': 'error',
|
||||
'import/no-anonymous-default-export': 'error',
|
||||
'import/no-default-export': 'error',
|
||||
'import/no-duplicates': 'error',
|
||||
'import/no-named-default': 'error',
|
||||
'import/no-namespace': 'error',
|
||||
'import/no-unassigned-import': 'error',
|
||||
// 'import/order': [
|
||||
// 'error',
|
||||
// {
|
||||
// groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object', 'type'],
|
||||
// 'newlines-between': 'always',
|
||||
// pathGroups: [
|
||||
// {
|
||||
// pattern: '@?*/**',
|
||||
// group: 'external',
|
||||
// position: 'after',
|
||||
// },
|
||||
// {
|
||||
// pattern: '@/**',
|
||||
// group: 'external',
|
||||
// position: 'after',
|
||||
// },
|
||||
// ],
|
||||
// alphabetize: {
|
||||
// order: 'asc' /* sort in ascending order. Options: ['ignore', 'asc', 'desc'] */,
|
||||
// caseInsensitive: true /* ignore case. Options: [true, false] */,
|
||||
// },
|
||||
// distinctGroup: true,
|
||||
// },
|
||||
// ],
|
||||
'import/prefer-default-export': 'off',
|
||||
// n
|
||||
'n/handle-callback-err': 'error',
|
||||
'n/no-callback-literal': 'error',
|
||||
'n/no-exports-assign': 'error',
|
||||
// 'n/no-extraneous-import': 'error',
|
||||
'n/no-extraneous-require': 'error',
|
||||
'n/no-hide-core-modules': 'error',
|
||||
'n/no-missing-import': 'off', // not compatible with typescript
|
||||
'n/no-missing-require': 'error',
|
||||
'n/no-new-require': 'error',
|
||||
'n/no-path-concat': 'error',
|
||||
// 'n/no-process-exit': 'error',
|
||||
'n/no-unpublished-bin': 'error',
|
||||
'n/no-unpublished-import': 'off', // TODO need to exclude seeds
|
||||
'n/no-unpublished-require': 'error',
|
||||
'n/no-unsupported-features': ['error', { ignores: ['modules'] }],
|
||||
'n/no-unsupported-features/es-builtins': 'error',
|
||||
'n/no-unsupported-features/es-syntax': 'error',
|
||||
'n/no-unsupported-features/node-builtins': 'error',
|
||||
'n/process-exit-as-throw': 'error',
|
||||
'n/shebang': 'error',
|
||||
'n/callback-return': 'error',
|
||||
'n/exports-style': 'error',
|
||||
'n/file-extension-in-import': 'off',
|
||||
'n/global-require': 'error',
|
||||
'n/no-mixed-requires': 'error',
|
||||
'n/no-process-env': 'error',
|
||||
'n/no-restricted-import': 'error',
|
||||
'n/no-restricted-require': 'error',
|
||||
// 'n/no-sync': 'error',
|
||||
'n/prefer-global/buffer': 'error',
|
||||
'n/prefer-global/console': 'error',
|
||||
'n/prefer-global/process': 'error',
|
||||
'n/prefer-global/text-decoder': 'error',
|
||||
'n/prefer-global/text-encoder': 'error',
|
||||
'n/prefer-global/url': 'error',
|
||||
'n/prefer-global/url-search-params': 'error',
|
||||
'n/prefer-promises/dns': 'error',
|
||||
// 'n/prefer-promises/fs': 'error',
|
||||
// promise
|
||||
// 'promise/catch-or-return': 'error',
|
||||
// 'promise/no-return-wrap': 'error',
|
||||
// 'promise/param-names': 'error',
|
||||
// 'promise/always-return': 'error',
|
||||
// 'promise/no-native': 'off',
|
||||
// 'promise/no-nesting': 'warn',
|
||||
// 'promise/no-promise-in-callback': 'warn',
|
||||
// 'promise/no-callback-in-promise': 'warn',
|
||||
// 'promise/avoid-new': 'warn',
|
||||
// 'promise/no-new-statics': 'error',
|
||||
// 'promise/no-return-in-finally': 'warn',
|
||||
// 'promise/valid-params': 'warn',
|
||||
// 'promise/prefer-await-to-callbacks': 'error',
|
||||
// 'promise/no-multiple-resolved': 'error',
|
||||
// eslint comments
|
||||
'@eslint-community/eslint-comments/disable-enable-pair': ['error', { allowWholeFile: true }],
|
||||
'@eslint-community/eslint-comments/no-restricted-disable': 'error',
|
||||
'@eslint-community/eslint-comments/no-use': 'off',
|
||||
'@eslint-community/eslint-comments/require-description': 'off',
|
||||
},
|
||||
overrides: [
|
||||
// only for ts files
|
||||
{
|
||||
files: ['*.ts', '*.tsx'],
|
||||
extends: [
|
||||
// 'plugin:@typescript-eslint/recommended',
|
||||
// 'plugin:@typescript-eslint/recommended-requiring-type-checking',
|
||||
// 'plugin:@typescript-eslint/strict',
|
||||
],
|
||||
rules: {
|
||||
// allow explicitly defined dangling promises
|
||||
// '@typescript-eslint/no-floating-promises': ['error', { ignoreVoid: true }],
|
||||
'no-void': ['error', { allowAsStatement: true }],
|
||||
// ignore prefer-regexp-exec rule to allow string.match(regex)
|
||||
'@typescript-eslint/prefer-regexp-exec': 'off',
|
||||
// this should not run on ts files: https://github.com/import-js/eslint-plugin-import/issues/2215#issuecomment-911245486
|
||||
'import/unambiguous': 'off',
|
||||
// this is not compatible with typeorm, due to joined tables can be null, but are not defined as nullable
|
||||
'@typescript-eslint/no-unnecessary-condition': 'off',
|
||||
},
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: ['./tsconfig.json'],
|
||||
// this is to properly reference the referenced project database without requirement of compiling it
|
||||
// eslint-disable-next-line camelcase
|
||||
EXPERIMENTAL_useSourceOfProjectReferenceRedirect: true,
|
||||
},
|
||||
},
|
||||
// we do not have testing on the database
|
||||
// {
|
||||
// files: ['*.test.ts'],
|
||||
// plugins: ['jest'],
|
||||
// rules: {
|
||||
// 'jest/no-disabled-tests': 'error',
|
||||
// 'jest/no-focused-tests': 'error',
|
||||
// 'jest/no-identical-title': 'error',
|
||||
// 'jest/prefer-to-have-length': 'error',
|
||||
// 'jest/valid-expect': 'error',
|
||||
// '@typescript-eslint/unbound-method': 'off',
|
||||
// 'jest/unbound-method': 'error',
|
||||
// },
|
||||
// },
|
||||
],
|
||||
}
|
||||
27
dlt-database/.gitignore
vendored
Normal file
27
dlt-database/.gitignore
vendored
Normal file
@ -0,0 +1,27 @@
|
||||
.DS_Store
|
||||
node_modules/
|
||||
build/
|
||||
.cache/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
test/unit/coverage
|
||||
|
||||
package-lock.json
|
||||
/.env
|
||||
/.env.bak
|
||||
.env.development.local
|
||||
.env.production.local
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
|
||||
# coverage folder
|
||||
|
||||
coverage/
|
||||
|
||||
*~
|
||||
9
dlt-database/.prettierrc.js
Normal file
9
dlt-database/.prettierrc.js
Normal file
@ -0,0 +1,9 @@
|
||||
module.exports = {
|
||||
semi: false,
|
||||
printWidth: 100,
|
||||
singleQuote: true,
|
||||
trailingComma: "all",
|
||||
tabWidth: 2,
|
||||
bracketSpacing: true,
|
||||
endOfLine: "auto",
|
||||
};
|
||||
130
dlt-database/Dockerfile
Normal file
130
dlt-database/Dockerfile
Normal file
@ -0,0 +1,130 @@
|
||||
##################################################################################
|
||||
# BASE ###########################################################################
|
||||
##################################################################################
|
||||
FROM node:18.7.0-alpine3.16 as base
|
||||
|
||||
# ENVs (available in production aswell, can be overwritten by commandline or env file)
|
||||
## DOCKER_WORKDIR would be a classical ARG, but that is not multi layer persistent - shame
|
||||
ENV DOCKER_WORKDIR="/app"
|
||||
## We Cannot do `$(date -u +'%Y-%m-%dT%H:%M:%SZ')` here so we use unix timestamp=0
|
||||
ENV BUILD_DATE="1970-01-01T00:00:00.00Z"
|
||||
## We cannot do $(npm run version).${BUILD_NUMBER} here so we default to 0.0.0.0
|
||||
ENV BUILD_VERSION="0.0.0.0"
|
||||
## We cannot do `$(git rev-parse --short HEAD)` here so we default to 0000000
|
||||
ENV BUILD_COMMIT="0000000"
|
||||
## SET NODE_ENV
|
||||
ENV NODE_ENV="production"
|
||||
|
||||
# Labels
|
||||
LABEL org.label-schema.build-date="${BUILD_DATE}"
|
||||
LABEL org.label-schema.name="gradido:database"
|
||||
LABEL org.label-schema.description="Gradido Database Migration Service"
|
||||
LABEL org.label-schema.usage="https://github.com/gradido/gradido/blob/master/README.md"
|
||||
LABEL org.label-schema.url="https://gradido.net"
|
||||
LABEL org.label-schema.vcs-url="https://github.com/gradido/gradido/tree/master/database"
|
||||
LABEL org.label-schema.vcs-ref="${BUILD_COMMIT}"
|
||||
LABEL org.label-schema.vendor="Gradido Community"
|
||||
LABEL org.label-schema.version="${BUILD_VERSION}"
|
||||
LABEL org.label-schema.schema-version="1.0"
|
||||
LABEL maintainer="support@gradido.net"
|
||||
|
||||
# Install Additional Software
|
||||
## install: git
|
||||
#RUN apk --no-cache add git
|
||||
|
||||
## Workdir
|
||||
RUN mkdir -p ${DOCKER_WORKDIR}
|
||||
WORKDIR ${DOCKER_WORKDIR}
|
||||
|
||||
##################################################################################
|
||||
# DEVELOPMENT (Connected to the local environment, to reload on demand) ##########
|
||||
##################################################################################
|
||||
FROM base as development
|
||||
|
||||
# We don't need to copy or build anything since we gonna bind to the
|
||||
# local filesystem which will need a rebuild anyway
|
||||
|
||||
# Run command
|
||||
# (for development we need to execute npm install since the
|
||||
# node_modules are on another volume and need updating)
|
||||
CMD /bin/sh -c "yarn install"
|
||||
|
||||
##################################################################################
|
||||
# BUILD (Does contain all files and is therefore bloated) ########################
|
||||
##################################################################################
|
||||
FROM base as build
|
||||
|
||||
# Copy everything
|
||||
COPY . .
|
||||
# npm install
|
||||
RUN yarn install --production=false --frozen-lockfile --non-interactive
|
||||
# npm build
|
||||
RUN yarn run build
|
||||
|
||||
##################################################################################
|
||||
# TEST UP ########################################################################
|
||||
##################################################################################
|
||||
FROM build as test_up
|
||||
|
||||
# Run command
|
||||
CMD /bin/sh -c "yarn install && yarn run dev_up"
|
||||
|
||||
##################################################################################
|
||||
# TEST RESET #####################################################################
|
||||
##################################################################################
|
||||
FROM build as test_reset
|
||||
|
||||
# Run command
|
||||
CMD /bin/sh -c "yarn install && yarn run dev_reset"
|
||||
|
||||
##################################################################################
|
||||
# TEST DOWN ######################################################################
|
||||
##################################################################################
|
||||
FROM build as test_down
|
||||
|
||||
# Run command
|
||||
CMD /bin/sh -c "yarn install && yarn run dev_down"
|
||||
|
||||
##################################################################################
|
||||
# PRODUCTION (Does contain only "binary"- and static-files to reduce image size) #
|
||||
##################################################################################
|
||||
FROM base as production
|
||||
|
||||
# Copy "binary"-files from build image
|
||||
COPY --from=build ${DOCKER_WORKDIR}/build ./build
|
||||
# We also copy the node_modules express and serve-static for the run script
|
||||
COPY --from=build ${DOCKER_WORKDIR}/node_modules ./node_modules
|
||||
# Copy static files
|
||||
# COPY --from=build ${DOCKER_WORKDIR}/public ./public
|
||||
# Copy package.json for script definitions (lock file should not be needed)
|
||||
COPY --from=build ${DOCKER_WORKDIR}/package.json ./package.json
|
||||
# Copy Mnemonic files
|
||||
COPY --from=build ${DOCKER_WORKDIR}/src/config/*.txt ./src/config/
|
||||
# Copy log folder
|
||||
COPY --from=build ${DOCKER_WORKDIR}/log ./log
|
||||
# Copy run scripts run/
|
||||
# COPY --from=build ${DOCKER_WORKDIR}/run ./run
|
||||
|
||||
##################################################################################
|
||||
# PRODUCTION UP ##################################################################
|
||||
##################################################################################
|
||||
FROM production as production_up
|
||||
|
||||
# Run command
|
||||
CMD /bin/sh -c "yarn run up"
|
||||
|
||||
##################################################################################
|
||||
# PRODUCTION RESET ###############################################################
|
||||
##################################################################################
|
||||
FROM production as production_reset
|
||||
|
||||
# Run command
|
||||
CMD /bin/sh -c "yarn run reset"
|
||||
|
||||
##################################################################################
|
||||
# PRODUCTION DOWN ################################################################
|
||||
##################################################################################
|
||||
FROM production as production_down
|
||||
|
||||
# Run command
|
||||
CMD /bin/sh -c "yarn run down"
|
||||
39
dlt-database/README.md
Normal file
39
dlt-database/README.md
Normal file
@ -0,0 +1,39 @@
|
||||
# database
|
||||
|
||||
## Project setup
|
||||
|
||||
```bash
|
||||
yarn install
|
||||
```
|
||||
|
||||
## Upgrade migrations production
|
||||
|
||||
```bash
|
||||
yarn up
|
||||
```
|
||||
|
||||
## Upgrade migrations development
|
||||
|
||||
```bash
|
||||
yarn dev_up
|
||||
```
|
||||
|
||||
## Downgrade migrations production
|
||||
|
||||
```bash
|
||||
yarn down
|
||||
```
|
||||
|
||||
## Downgrade migrations development
|
||||
|
||||
```bash
|
||||
yarn dev_down
|
||||
```
|
||||
|
||||
## Reset database
|
||||
|
||||
```bash
|
||||
yarn dev_reset
|
||||
```
|
||||
|
||||
Runs all down migrations and after this all up migrations.
|
||||
79
dlt-database/entity/0001-init_db/Account.ts
Normal file
79
dlt-database/entity/0001-init_db/Account.ts
Normal file
@ -0,0 +1,79 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
OneToMany,
|
||||
BaseEntity,
|
||||
} from 'typeorm'
|
||||
import { User } from '../User'
|
||||
import { TransactionRecipe } from '../TransactionRecipe'
|
||||
import { ConfirmedTransaction } from '../ConfirmedTransaction'
|
||||
import { DecimalTransformer } from '../../src/typeorm/DecimalTransformer'
|
||||
import { Decimal } from 'decimal.js-light'
|
||||
import { AccountCommunity } from '../AccountCommunity'
|
||||
|
||||
@Entity('accounts')
|
||||
export class Account extends BaseEntity {
|
||||
@PrimaryGeneratedColumn('increment', { unsigned: true })
|
||||
id: number
|
||||
|
||||
@ManyToOne(() => User, (user) => user.accounts) // Assuming you have a User entity with 'accounts' relation
|
||||
@JoinColumn({ name: 'user_id' })
|
||||
user?: User
|
||||
|
||||
// if user id is null, account belongs to community gmw or auf
|
||||
@Column({ name: 'user_id', type: 'int', unsigned: true, nullable: true })
|
||||
userId?: number
|
||||
|
||||
@Column({ name: 'derivation_index', type: 'int', unsigned: true })
|
||||
derivationIndex: number
|
||||
|
||||
@Column({ name: 'derive2_pubkey', type: 'binary', length: 32, unique: true })
|
||||
derive2Pubkey: Buffer
|
||||
|
||||
@Column({ type: 'tinyint', unsigned: true })
|
||||
type: number
|
||||
|
||||
@Column({
|
||||
name: 'created_at',
|
||||
type: 'datetime',
|
||||
precision: 3,
|
||||
default: () => 'CURRENT_TIMESTAMP(3)',
|
||||
})
|
||||
createdAt: Date
|
||||
|
||||
@Column({ name: 'confirmed_at', type: 'datetime', precision: 3, nullable: true })
|
||||
confirmedAt?: Date
|
||||
|
||||
@Column({
|
||||
type: 'decimal',
|
||||
precision: 40,
|
||||
scale: 20,
|
||||
default: 0,
|
||||
transformer: DecimalTransformer,
|
||||
})
|
||||
balance: Decimal
|
||||
|
||||
@Column({
|
||||
name: 'balance_date',
|
||||
type: 'datetime',
|
||||
precision: 3,
|
||||
default: () => 'CURRENT_TIMESTAMP(3)',
|
||||
})
|
||||
balanceDate: Date
|
||||
|
||||
@OneToMany(() => AccountCommunity, (accountCommunity) => accountCommunity.account)
|
||||
@JoinColumn({ name: 'account_id' })
|
||||
accountCommunities: AccountCommunity[]
|
||||
|
||||
@OneToMany(() => TransactionRecipe, (recipe) => recipe.signingAccount)
|
||||
transactionRecipesSigning?: TransactionRecipe[]
|
||||
|
||||
@OneToMany(() => TransactionRecipe, (recipe) => recipe.recipientAccount)
|
||||
transactionRecipesRecipient?: TransactionRecipe[]
|
||||
|
||||
@OneToMany(() => ConfirmedTransaction, (transaction) => transaction.account)
|
||||
confirmedTransactions?: ConfirmedTransaction[]
|
||||
}
|
||||
30
dlt-database/entity/0001-init_db/AccountCommunity.ts
Normal file
30
dlt-database/entity/0001-init_db/AccountCommunity.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, BaseEntity } from 'typeorm'
|
||||
|
||||
import { Account } from '../Account'
|
||||
import { Community } from '../Community'
|
||||
|
||||
@Entity('accounts_communities')
|
||||
export class AccountCommunity extends BaseEntity {
|
||||
@PrimaryGeneratedColumn('increment', { unsigned: true })
|
||||
id: number
|
||||
|
||||
@ManyToOne(() => Account, (account) => account.accountCommunities)
|
||||
@JoinColumn({ name: 'account_id' })
|
||||
account: Account
|
||||
|
||||
@Column({ name: 'account_id', type: 'int', unsigned: true })
|
||||
accountId: number
|
||||
|
||||
@ManyToOne(() => Community, (community) => community.accountCommunities)
|
||||
@JoinColumn({ name: 'community_id' })
|
||||
community: Community
|
||||
|
||||
@Column({ name: 'community_id', type: 'int', unsigned: true })
|
||||
communityId: number
|
||||
|
||||
@Column({ name: 'valid_from', type: 'datetime', precision: 3 })
|
||||
validFrom: Date
|
||||
|
||||
@Column({ name: 'valid_to', type: 'datetime', precision: 3, nullable: true })
|
||||
validTo?: Date
|
||||
}
|
||||
68
dlt-database/entity/0001-init_db/Community.ts
Normal file
68
dlt-database/entity/0001-init_db/Community.ts
Normal file
@ -0,0 +1,68 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
JoinColumn,
|
||||
OneToOne,
|
||||
OneToMany,
|
||||
BaseEntity,
|
||||
} from 'typeorm'
|
||||
import { Account } from '../Account'
|
||||
import { TransactionRecipe } from '../TransactionRecipe'
|
||||
import { AccountCommunity } from '../AccountCommunity'
|
||||
|
||||
@Entity('communities')
|
||||
export class Community extends BaseEntity {
|
||||
@PrimaryGeneratedColumn('increment', { unsigned: true })
|
||||
id: number
|
||||
|
||||
@Column({ name: 'iota_topic', collation: 'utf8mb4_unicode_ci' })
|
||||
iotaTopic: string
|
||||
|
||||
@Column({ name: 'root_pubkey', type: 'binary', length: 32, unique: true })
|
||||
rootPubkey: Buffer
|
||||
|
||||
@Column({ name: 'root_privkey', type: 'binary', length: 32, nullable: true })
|
||||
rootPrivkey?: Buffer
|
||||
|
||||
@Column({ name: 'root_chaincode', type: 'binary', length: 32, nullable: true })
|
||||
rootChaincode?: Buffer
|
||||
|
||||
@Column({ type: 'tinyint', default: true })
|
||||
foreign: boolean
|
||||
|
||||
@Column({ name: 'gmw_account_id', type: 'int', unsigned: true, nullable: true })
|
||||
gmwAccountId?: number
|
||||
|
||||
@OneToOne(() => Account)
|
||||
@JoinColumn({ name: 'gmw_account_id' })
|
||||
gmwAccount?: Account
|
||||
|
||||
@Column({ name: 'auf_account_id', type: 'int', unsigned: true, nullable: true })
|
||||
aufAccountId?: number
|
||||
|
||||
@OneToOne(() => Account)
|
||||
@JoinColumn({ name: 'auf_account_id' })
|
||||
aufAccount?: Account
|
||||
|
||||
@Column({
|
||||
name: 'created_at',
|
||||
type: 'datetime',
|
||||
precision: 3,
|
||||
default: () => 'CURRENT_TIMESTAMP(3)',
|
||||
})
|
||||
createdAt: Date
|
||||
|
||||
@Column({ name: 'confirmed_at', type: 'datetime', precision: 3, nullable: true })
|
||||
confirmedAt?: Date
|
||||
|
||||
@OneToMany(() => AccountCommunity, (accountCommunity) => accountCommunity.community)
|
||||
@JoinColumn({ name: 'community_id' })
|
||||
accountCommunities: AccountCommunity[]
|
||||
|
||||
@OneToMany(() => TransactionRecipe, (recipe) => recipe.senderCommunity)
|
||||
transactionRecipesSender?: TransactionRecipe[]
|
||||
|
||||
@OneToMany(() => TransactionRecipe, (recipe) => recipe.recipientCommunity)
|
||||
transactionRecipesRecipient?: TransactionRecipe[]
|
||||
}
|
||||
57
dlt-database/entity/0001-init_db/ConfirmedTransaction.ts
Normal file
57
dlt-database/entity/0001-init_db/ConfirmedTransaction.ts
Normal file
@ -0,0 +1,57 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
OneToOne,
|
||||
BaseEntity,
|
||||
} from 'typeorm'
|
||||
import { Decimal } from 'decimal.js-light'
|
||||
|
||||
import { DecimalTransformer } from '../../src/typeorm/DecimalTransformer'
|
||||
import { Account } from '../Account'
|
||||
import { TransactionRecipe } from '../TransactionRecipe'
|
||||
|
||||
@Entity('confirmed_transactions')
|
||||
export class ConfirmedTransaction extends BaseEntity {
|
||||
@PrimaryGeneratedColumn('increment', { unsigned: true, type: 'bigint' })
|
||||
id: number
|
||||
|
||||
@OneToOne(() => TransactionRecipe, (recipe) => recipe.confirmedTransaction)
|
||||
@JoinColumn({ name: 'transaction_recipe_id' })
|
||||
transactionRecipe: TransactionRecipe
|
||||
|
||||
@Column({ name: 'transaction_recipe_id', type: 'int', unsigned: true })
|
||||
transactionRecipeId: number
|
||||
|
||||
@Column({ type: 'bigint' })
|
||||
nr: number
|
||||
|
||||
@Column({ type: 'binary', length: 48 })
|
||||
runningHash: Buffer
|
||||
|
||||
@ManyToOne(() => Account, (account) => account.confirmedTransactions)
|
||||
@JoinColumn({ name: 'account_id' })
|
||||
account: Account
|
||||
|
||||
@Column({ name: 'account_id', type: 'int', unsigned: true })
|
||||
accountId: number
|
||||
|
||||
@Column({
|
||||
name: 'account_balance',
|
||||
type: 'decimal',
|
||||
precision: 40,
|
||||
scale: 20,
|
||||
nullable: false,
|
||||
default: 0,
|
||||
transformer: DecimalTransformer,
|
||||
})
|
||||
accountBalance: Decimal
|
||||
|
||||
@Column({ name: 'iota_milestone', type: 'bigint' })
|
||||
iotaMilestone: number
|
||||
|
||||
@Column({ name: 'confirmed_at', type: 'datetime', precision: 3 })
|
||||
confirmedAt: Date
|
||||
}
|
||||
10
dlt-database/entity/0001-init_db/InvalidTransaction.ts
Normal file
10
dlt-database/entity/0001-init_db/InvalidTransaction.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { Entity, PrimaryGeneratedColumn, Column, BaseEntity } from 'typeorm'
|
||||
|
||||
@Entity('invalid_transactions')
|
||||
export class InvalidTransaction extends BaseEntity {
|
||||
@PrimaryGeneratedColumn('increment', { unsigned: true, type: 'bigint' })
|
||||
id: number
|
||||
|
||||
@Column({ name: 'iota_message_id', type: 'binary', length: 32 })
|
||||
iotaMessageId: Buffer
|
||||
}
|
||||
13
dlt-database/entity/0001-init_db/Migration.ts
Normal file
13
dlt-database/entity/0001-init_db/Migration.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { BaseEntity, Entity, PrimaryGeneratedColumn, Column } from 'typeorm'
|
||||
|
||||
@Entity('migrations')
|
||||
export class Migration extends BaseEntity {
|
||||
@PrimaryGeneratedColumn() // This is actually not a primary column
|
||||
version: number
|
||||
|
||||
@Column({ length: 256, nullable: true, default: null })
|
||||
fileName: string
|
||||
|
||||
@Column({ type: 'datetime', default: () => 'CURRENT_TIMESTAMP' })
|
||||
date: Date
|
||||
}
|
||||
80
dlt-database/entity/0001-init_db/TransactionRecipe.ts
Normal file
80
dlt-database/entity/0001-init_db/TransactionRecipe.ts
Normal file
@ -0,0 +1,80 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
ManyToOne,
|
||||
OneToOne,
|
||||
JoinColumn,
|
||||
BaseEntity,
|
||||
} from 'typeorm'
|
||||
import { Decimal } from 'decimal.js-light'
|
||||
|
||||
import { DecimalTransformer } from '../../src/typeorm/DecimalTransformer'
|
||||
import { Account } from '../Account'
|
||||
import { Community } from '../Community'
|
||||
import { ConfirmedTransaction } from '../ConfirmedTransaction'
|
||||
|
||||
@Entity('transaction_recipes')
|
||||
export class TransactionRecipe extends BaseEntity {
|
||||
@PrimaryGeneratedColumn('increment', { unsigned: true, type: 'bigint' })
|
||||
id: number
|
||||
|
||||
@Column({ name: 'iota_message_id', type: 'binary', length: 32, nullable: true })
|
||||
iotaMessageId?: Buffer
|
||||
|
||||
// if transaction has a sender than it is also the sender account
|
||||
@ManyToOne(() => Account, (account) => account.transactionRecipesSigning)
|
||||
@JoinColumn({ name: 'signing_account_id' })
|
||||
signingAccount: Account
|
||||
|
||||
@Column({ name: 'signing_account_id', type: 'int', unsigned: true })
|
||||
signingAccountId: number
|
||||
|
||||
@ManyToOne(() => Account, (account) => account.transactionRecipesRecipient)
|
||||
@JoinColumn({ name: 'recipient_account_id' })
|
||||
recipientAccount?: Account
|
||||
|
||||
@Column({ name: 'recipient_account_id', type: 'int', unsigned: true, nullable: true })
|
||||
recipientAccountId?: number
|
||||
|
||||
@ManyToOne(() => Community, (community) => community.transactionRecipesSender)
|
||||
@JoinColumn({ name: 'sender_community_id' })
|
||||
senderCommunity: Community
|
||||
|
||||
@Column({ name: 'sender_community_id', type: 'int', unsigned: true })
|
||||
senderCommunityId: number
|
||||
|
||||
@ManyToOne(() => Community, (community) => community.transactionRecipesRecipient)
|
||||
@JoinColumn({ name: 'recipient_community_id' })
|
||||
recipientCommunity?: Community
|
||||
|
||||
@Column({ name: 'recipient_community_id', type: 'int', unsigned: true, nullable: true })
|
||||
recipientCommunityId?: number
|
||||
|
||||
@Column({
|
||||
type: 'decimal',
|
||||
precision: 40,
|
||||
scale: 20,
|
||||
nullable: true,
|
||||
transformer: DecimalTransformer,
|
||||
})
|
||||
amount?: Decimal
|
||||
|
||||
@Column({ type: 'tinyint' })
|
||||
type: number
|
||||
|
||||
@Column({ name: 'created_at', type: 'datetime', precision: 3 })
|
||||
createdAt: Date
|
||||
|
||||
@Column({ name: 'body_bytes', type: 'blob' })
|
||||
bodyBytes: Buffer
|
||||
|
||||
@Column({ type: 'binary', length: 64 })
|
||||
signature: Buffer
|
||||
|
||||
@Column({ name: 'protocol_version', type: 'int', default: 1 })
|
||||
protocolVersion: number
|
||||
|
||||
@OneToOne(() => ConfirmedTransaction, (transaction) => transaction.transactionRecipe)
|
||||
confirmedTransaction?: ConfirmedTransaction
|
||||
}
|
||||
40
dlt-database/entity/0001-init_db/User.ts
Normal file
40
dlt-database/entity/0001-init_db/User.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import { BaseEntity, Entity, PrimaryGeneratedColumn, Column, OneToMany, JoinColumn } from 'typeorm'
|
||||
|
||||
import { Account } from '../Account'
|
||||
|
||||
@Entity('users', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' })
|
||||
export class User extends BaseEntity {
|
||||
@PrimaryGeneratedColumn('increment', { unsigned: true })
|
||||
id: number
|
||||
|
||||
@Column({
|
||||
name: 'gradido_id',
|
||||
length: 36,
|
||||
nullable: true,
|
||||
collation: 'utf8mb4_unicode_ci',
|
||||
})
|
||||
gradidoID?: string
|
||||
|
||||
@Column({ name: 'derive1_pubkey', type: 'binary', length: 32, unique: true })
|
||||
derive1Pubkey: Buffer
|
||||
|
||||
@Column({
|
||||
name: 'created_at',
|
||||
type: 'datetime',
|
||||
precision: 3,
|
||||
default: () => 'CURRENT_TIMESTAMP(3)',
|
||||
})
|
||||
createdAt: Date
|
||||
|
||||
@Column({
|
||||
name: 'confirmed_at',
|
||||
type: 'datetime',
|
||||
precision: 3,
|
||||
nullable: true,
|
||||
})
|
||||
confirmedAt?: Date
|
||||
|
||||
@OneToMany(() => Account, (account) => account.user)
|
||||
@JoinColumn({ name: 'user_id' })
|
||||
accounts?: Account[]
|
||||
}
|
||||
78
dlt-database/entity/0002-refactor_add_community/Account.ts
Normal file
78
dlt-database/entity/0002-refactor_add_community/Account.ts
Normal file
@ -0,0 +1,78 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
OneToMany,
|
||||
BaseEntity,
|
||||
} from 'typeorm'
|
||||
import { User } from '../User'
|
||||
import { TransactionRecipe } from '../TransactionRecipe'
|
||||
import { ConfirmedTransaction } from '../ConfirmedTransaction'
|
||||
import { DecimalTransformer } from '../../src/typeorm/DecimalTransformer'
|
||||
import { Decimal } from 'decimal.js-light'
|
||||
import { AccountCommunity } from '../AccountCommunity'
|
||||
|
||||
@Entity('accounts')
|
||||
export class Account extends BaseEntity {
|
||||
@PrimaryGeneratedColumn('increment', { unsigned: true })
|
||||
id: number
|
||||
|
||||
@ManyToOne(() => User, (user) => user.accounts) // Assuming you have a User entity with 'accounts' relation
|
||||
@JoinColumn({ name: 'user_id' })
|
||||
user?: User
|
||||
|
||||
// if user id is null, account belongs to community gmw or auf
|
||||
@Column({ name: 'user_id', type: 'int', unsigned: true, nullable: true })
|
||||
userId?: number
|
||||
|
||||
@Column({ name: 'derivation_index', type: 'int', unsigned: true })
|
||||
derivationIndex: number
|
||||
|
||||
@Column({ name: 'derive2_pubkey', type: 'binary', length: 32, unique: true })
|
||||
derive2Pubkey: Buffer
|
||||
|
||||
@Column({ type: 'tinyint', unsigned: true })
|
||||
type: number
|
||||
|
||||
@Column({
|
||||
name: 'created_at',
|
||||
type: 'datetime',
|
||||
precision: 3,
|
||||
default: () => 'CURRENT_TIMESTAMP(3)',
|
||||
})
|
||||
createdAt: Date
|
||||
|
||||
@Column({ name: 'confirmed_at', type: 'datetime', nullable: true })
|
||||
confirmedAt?: Date
|
||||
|
||||
@Column({
|
||||
type: 'decimal',
|
||||
precision: 40,
|
||||
scale: 20,
|
||||
default: 0,
|
||||
transformer: DecimalTransformer,
|
||||
})
|
||||
balance: Decimal
|
||||
|
||||
@Column({
|
||||
name: 'balance_date',
|
||||
type: 'datetime',
|
||||
default: () => 'CURRENT_TIMESTAMP()',
|
||||
})
|
||||
balanceDate: Date
|
||||
|
||||
@OneToMany(() => AccountCommunity, (accountCommunity) => accountCommunity.account)
|
||||
@JoinColumn({ name: 'account_id' })
|
||||
accountCommunities: AccountCommunity[]
|
||||
|
||||
@OneToMany(() => TransactionRecipe, (recipe) => recipe.signingAccount)
|
||||
transactionRecipesSigning?: TransactionRecipe[]
|
||||
|
||||
@OneToMany(() => TransactionRecipe, (recipe) => recipe.recipientAccount)
|
||||
transactionRecipesRecipient?: TransactionRecipe[]
|
||||
|
||||
@OneToMany(() => ConfirmedTransaction, (transaction) => transaction.account)
|
||||
confirmedTransactions?: ConfirmedTransaction[]
|
||||
}
|
||||
@ -0,0 +1,30 @@
|
||||
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, BaseEntity } from 'typeorm'
|
||||
|
||||
import { Account } from '../Account'
|
||||
import { Community } from '../Community'
|
||||
|
||||
@Entity('accounts_communities')
|
||||
export class AccountCommunity extends BaseEntity {
|
||||
@PrimaryGeneratedColumn('increment', { unsigned: true })
|
||||
id: number
|
||||
|
||||
@ManyToOne(() => Account, (account) => account.accountCommunities)
|
||||
@JoinColumn({ name: 'account_id' })
|
||||
account: Account
|
||||
|
||||
@Column({ name: 'account_id', type: 'int', unsigned: true })
|
||||
accountId: number
|
||||
|
||||
@ManyToOne(() => Community, (community) => community.accountCommunities)
|
||||
@JoinColumn({ name: 'community_id' })
|
||||
community: Community
|
||||
|
||||
@Column({ name: 'community_id', type: 'int', unsigned: true })
|
||||
communityId: number
|
||||
|
||||
@Column({ name: 'valid_from', type: 'datetime' })
|
||||
validFrom: Date
|
||||
|
||||
@Column({ name: 'valid_to', type: 'datetime', nullable: true })
|
||||
validTo?: Date
|
||||
}
|
||||
68
dlt-database/entity/0002-refactor_add_community/Community.ts
Normal file
68
dlt-database/entity/0002-refactor_add_community/Community.ts
Normal file
@ -0,0 +1,68 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
JoinColumn,
|
||||
OneToOne,
|
||||
OneToMany,
|
||||
BaseEntity,
|
||||
} from 'typeorm'
|
||||
import { Account } from '../Account'
|
||||
import { TransactionRecipe } from '../TransactionRecipe'
|
||||
import { AccountCommunity } from '../AccountCommunity'
|
||||
|
||||
@Entity('communities')
|
||||
export class Community extends BaseEntity {
|
||||
@PrimaryGeneratedColumn('increment', { unsigned: true })
|
||||
id: number
|
||||
|
||||
@Column({ name: 'iota_topic', collation: 'utf8mb4_unicode_ci' })
|
||||
iotaTopic: string
|
||||
|
||||
@Column({ name: 'root_pubkey', type: 'binary', length: 32, unique: true, nullable: true })
|
||||
rootPubkey?: Buffer
|
||||
|
||||
@Column({ name: 'root_privkey', type: 'binary', length: 64, nullable: true })
|
||||
rootPrivkey?: Buffer
|
||||
|
||||
@Column({ name: 'root_chaincode', type: 'binary', length: 32, nullable: true })
|
||||
rootChaincode?: Buffer
|
||||
|
||||
@Column({ type: 'tinyint', default: true })
|
||||
foreign: boolean
|
||||
|
||||
@Column({ name: 'gmw_account_id', type: 'int', unsigned: true, nullable: true })
|
||||
gmwAccountId?: number
|
||||
|
||||
@OneToOne(() => Account)
|
||||
@JoinColumn({ name: 'gmw_account_id' })
|
||||
gmwAccount?: Account
|
||||
|
||||
@Column({ name: 'auf_account_id', type: 'int', unsigned: true, nullable: true })
|
||||
aufAccountId?: number
|
||||
|
||||
@OneToOne(() => Account)
|
||||
@JoinColumn({ name: 'auf_account_id' })
|
||||
aufAccount?: Account
|
||||
|
||||
@Column({
|
||||
name: 'created_at',
|
||||
type: 'datetime',
|
||||
precision: 3,
|
||||
default: () => 'CURRENT_TIMESTAMP(3)',
|
||||
})
|
||||
createdAt: Date
|
||||
|
||||
@Column({ name: 'confirmed_at', type: 'datetime', nullable: true })
|
||||
confirmedAt?: Date
|
||||
|
||||
@OneToMany(() => AccountCommunity, (accountCommunity) => accountCommunity.community)
|
||||
@JoinColumn({ name: 'community_id' })
|
||||
accountCommunities: AccountCommunity[]
|
||||
|
||||
@OneToMany(() => TransactionRecipe, (recipe) => recipe.senderCommunity)
|
||||
transactionRecipesSender?: TransactionRecipe[]
|
||||
|
||||
@OneToMany(() => TransactionRecipe, (recipe) => recipe.recipientCommunity)
|
||||
transactionRecipesRecipient?: TransactionRecipe[]
|
||||
}
|
||||
@ -0,0 +1,57 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
OneToOne,
|
||||
BaseEntity,
|
||||
} from 'typeorm'
|
||||
import { Decimal } from 'decimal.js-light'
|
||||
|
||||
import { DecimalTransformer } from '../../src/typeorm/DecimalTransformer'
|
||||
import { Account } from '../Account'
|
||||
import { TransactionRecipe } from '../TransactionRecipe'
|
||||
|
||||
@Entity('confirmed_transactions')
|
||||
export class ConfirmedTransaction extends BaseEntity {
|
||||
@PrimaryGeneratedColumn('increment', { unsigned: true, type: 'bigint' })
|
||||
id: number
|
||||
|
||||
@OneToOne(() => TransactionRecipe, (recipe) => recipe.confirmedTransaction)
|
||||
@JoinColumn({ name: 'transaction_recipe_id' })
|
||||
transactionRecipe: TransactionRecipe
|
||||
|
||||
@Column({ name: 'transaction_recipe_id', type: 'int', unsigned: true })
|
||||
transactionRecipeId: number
|
||||
|
||||
@Column({ type: 'bigint' })
|
||||
nr: number
|
||||
|
||||
@Column({ type: 'binary', length: 48 })
|
||||
runningHash: Buffer
|
||||
|
||||
@ManyToOne(() => Account, (account) => account.confirmedTransactions)
|
||||
@JoinColumn({ name: 'account_id' })
|
||||
account: Account
|
||||
|
||||
@Column({ name: 'account_id', type: 'int', unsigned: true })
|
||||
accountId: number
|
||||
|
||||
@Column({
|
||||
name: 'account_balance',
|
||||
type: 'decimal',
|
||||
precision: 40,
|
||||
scale: 20,
|
||||
nullable: false,
|
||||
default: 0,
|
||||
transformer: DecimalTransformer,
|
||||
})
|
||||
accountBalance: Decimal
|
||||
|
||||
@Column({ name: 'iota_milestone', type: 'bigint' })
|
||||
iotaMilestone: number
|
||||
|
||||
@Column({ name: 'confirmed_at', type: 'datetime' })
|
||||
confirmedAt: Date
|
||||
}
|
||||
39
dlt-database/entity/0002-refactor_add_community/User.ts
Normal file
39
dlt-database/entity/0002-refactor_add_community/User.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { BaseEntity, Entity, PrimaryGeneratedColumn, Column, OneToMany, JoinColumn } from 'typeorm'
|
||||
|
||||
import { Account } from '../Account'
|
||||
|
||||
@Entity('users', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' })
|
||||
export class User extends BaseEntity {
|
||||
@PrimaryGeneratedColumn('increment', { unsigned: true })
|
||||
id: number
|
||||
|
||||
@Column({
|
||||
name: 'gradido_id',
|
||||
length: 36,
|
||||
nullable: true,
|
||||
collation: 'utf8mb4_unicode_ci',
|
||||
})
|
||||
gradidoID?: string
|
||||
|
||||
@Column({ name: 'derive1_pubkey', type: 'binary', length: 32, unique: true })
|
||||
derive1Pubkey: Buffer
|
||||
|
||||
@Column({
|
||||
name: 'created_at',
|
||||
type: 'datetime',
|
||||
precision: 3,
|
||||
default: () => 'CURRENT_TIMESTAMP(3)',
|
||||
})
|
||||
createdAt: Date
|
||||
|
||||
@Column({
|
||||
name: 'confirmed_at',
|
||||
type: 'datetime',
|
||||
nullable: true,
|
||||
})
|
||||
confirmedAt?: Date
|
||||
|
||||
@OneToMany(() => Account, (account) => account.user)
|
||||
@JoinColumn({ name: 'user_id' })
|
||||
accounts?: Account[]
|
||||
}
|
||||
1
dlt-database/entity/Account.ts
Normal file
1
dlt-database/entity/Account.ts
Normal file
@ -0,0 +1 @@
|
||||
export { Account } from './0002-refactor_add_community/Account'
|
||||
1
dlt-database/entity/AccountCommunity.ts
Normal file
1
dlt-database/entity/AccountCommunity.ts
Normal file
@ -0,0 +1 @@
|
||||
export { AccountCommunity } from './0002-refactor_add_community/AccountCommunity'
|
||||
1
dlt-database/entity/Community.ts
Normal file
1
dlt-database/entity/Community.ts
Normal file
@ -0,0 +1 @@
|
||||
export { Community } from './0002-refactor_add_community/Community'
|
||||
1
dlt-database/entity/ConfirmedTransaction.ts
Normal file
1
dlt-database/entity/ConfirmedTransaction.ts
Normal file
@ -0,0 +1 @@
|
||||
export { ConfirmedTransaction } from './0002-refactor_add_community/ConfirmedTransaction'
|
||||
1
dlt-database/entity/InvalidTransaction.ts
Normal file
1
dlt-database/entity/InvalidTransaction.ts
Normal file
@ -0,0 +1 @@
|
||||
export { InvalidTransaction } from './0001-init_db/InvalidTransaction'
|
||||
1
dlt-database/entity/Migration.ts
Normal file
1
dlt-database/entity/Migration.ts
Normal file
@ -0,0 +1 @@
|
||||
export { Migration } from './0001-init_db/Migration'
|
||||
1
dlt-database/entity/TransactionRecipe.ts
Normal file
1
dlt-database/entity/TransactionRecipe.ts
Normal file
@ -0,0 +1 @@
|
||||
export { TransactionRecipe } from './0001-init_db/TransactionRecipe'
|
||||
1
dlt-database/entity/User.ts
Normal file
1
dlt-database/entity/User.ts
Normal file
@ -0,0 +1 @@
|
||||
export { User } from './0002-refactor_add_community/User'
|
||||
19
dlt-database/entity/index.ts
Normal file
19
dlt-database/entity/index.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { Account } from './Account'
|
||||
import { AccountCommunity } from './AccountCommunity'
|
||||
import { Community } from './Community'
|
||||
import { ConfirmedTransaction } from './ConfirmedTransaction'
|
||||
import { InvalidTransaction } from './InvalidTransaction'
|
||||
import { Migration } from './Migration'
|
||||
import { TransactionRecipe } from './TransactionRecipe'
|
||||
import { User } from './User'
|
||||
|
||||
export const entities = [
|
||||
AccountCommunity,
|
||||
Account,
|
||||
Community,
|
||||
ConfirmedTransaction,
|
||||
InvalidTransaction,
|
||||
Migration,
|
||||
TransactionRecipe,
|
||||
User,
|
||||
]
|
||||
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