Merge remote-tracking branch

'origin/3187-feature-x-sendcoind-23-invoke-settlement-of-x-pending-tx'
into 2947-refactor-the-existing-sendcoins-resolver-methode-to-distingue-between-local-transaction-and-x-transaction
This commit is contained in:
Claus-Peter Huebner 2023-09-28 23:07:43 +02:00
commit 371f4a2dfa
93 changed files with 3310 additions and 1169 deletions

View File

@ -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

View File

@ -7,7 +7,7 @@ module.exports = {
collectCoverageFrom: ['src/**/*.ts', '!**/node_modules/**', '!src/seeds/**', '!build/**'],
coverageThreshold: {
global: {
lines: 83,
lines: 85,
},
},
setupFiles: ['<rootDir>/test/testSetup.ts'],

View File

@ -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)
}
}
}

View File

@ -27,4 +27,4 @@ export const settleSendCoins = gql`
senderUserName: $senderUserName
)
}
*/
*/

View File

@ -96,8 +96,8 @@ export async function processXComPendingSendCoins(
pendingTx.amount = amount.mul(-1)
pendingTx.balance = senderBalance.balance
pendingTx.balanceDate = creationDate
pendingTx.decay = senderBalance.decay.decay
pendingTx.decayStart = senderBalance.decay.start
pendingTx.decay = senderBalance ? senderBalance.decay.decay : new Decimal(0)
pendingTx.decayStart = senderBalance ? senderBalance.decay.start : null
if (receiverCom.communityUuid) {
pendingTx.linkedUserCommunityUuid = receiverCom.communityUuid
}
@ -190,6 +190,7 @@ export async function processXComCommittingSendCoins(
const receiverFCom = await DbFederatedCommunity.findOneOrFail({
where: {
publicKey: receiverCom.publicKey,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
apiVersion: CONFIG.FEDERATION_BACKEND_SEND_ON_API,
},
})

View File

@ -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)

View File

@ -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(

View File

@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/restrict-template-expressions */
/* eslint-disable new-cap */
/* eslint-disable @typescript-eslint/no-non-null-assertion */
/*
import { getConnection } from '@dbTools/typeorm'
import { Community as DbCommunity } from '@entity/Community'
import { PendingTransaction as DbPendingTransaction } from '@entity/PendingTransaction'
@ -94,7 +94,7 @@ export async function settlePendingSenderTransaction(
await queryRunner.commitTransaction()
logger.info(`commit send Transaction successful...`)
/*
--*
await EVENT_TRANSACTION_SEND(sender, recipient, transactionSend, transactionSend.amount)
await EVENT_TRANSACTION_RECEIVE(
@ -103,7 +103,7 @@ export async function settlePendingSenderTransaction(
transactionReceive,
transactionReceive.amount,
)
*/
*--
// trigger to send transaction via dlt-connector
// void sendTransactionsToDltConnector()
} catch (e) {
@ -113,7 +113,7 @@ export async function settlePendingSenderTransaction(
await queryRunner.release()
releaseLock()
}
/*
--*
void sendTransactionReceivedEmail({
firstName: recipient.firstName,
lastName: recipient.lastName,
@ -141,6 +141,7 @@ export async function settlePendingSenderTransaction(
} finally {
releaseLock()
}
*/
*--
return true
}
*/

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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',
},
}
/*

View File

@ -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": {

View File

@ -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,
}

View 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'),
})
})
})
})

View 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)
}

View 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
}

View 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
}

View 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',
)
})
})
})

View 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
}
}

View 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
}

View 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,
}

View 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',
})

View 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',
})

View 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
}

View 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
}

View File

@ -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')

View 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
}

View 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
}

View 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
}

View 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)
})
})
})

View 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
}
}
}
}

View File

@ -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',
},
})
})
})

View File

@ -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
}
}
}
}

View File

@ -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',

View File

@ -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,
},
})
}

View 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`
},
},
})
}
}

View 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}`
},
},
})
}
}

View 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
}

View 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',
),
)
})
})

View 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
}

View 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?
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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)
})
})

View 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
}

View 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)
})
})

View 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
}

View 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
}

View 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
}

View File

@ -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)
})
})

View File

@ -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
}

View 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 }

View 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

View 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')
}

View 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()
}
}

View File

@ -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

View File

@ -2,28 +2,26 @@ import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
ManyToOne,
JoinColumn,
OneToMany,
ManyToMany,
JoinTable,
BaseEntity,
} from 'typeorm'
import { User } from './User'
import { Community } from './Community'
import { TransactionRecipe } from './TransactionRecipe'
import { ConfirmedTransaction } from './ConfirmedTransaction'
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 {
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
user?: User
// if user id is null, account belongs to community gmw or auf
@Column({ name: 'user_id', type: 'int', unsigned: true, nullable: true })
@ -38,10 +36,15 @@ export class Account {
@Column({ type: 'tinyint', unsigned: true })
type: number
@CreateDateColumn({ name: 'created_at', type: 'datetime', default: () => 'CURRENT_TIMESTAMP(3)' })
@Column({
name: 'created_at',
type: 'datetime',
precision: 3,
default: () => 'CURRENT_TIMESTAMP(3)',
})
createdAt: Date
@Column({ name: 'confirmed_at', type: 'datetime', nullable: true })
@Column({ name: 'confirmed_at', type: 'datetime', precision: 3, nullable: true })
confirmedAt?: Date
@Column({
@ -56,17 +59,14 @@ export class Account {
@Column({
name: 'balance_date',
type: 'datetime',
precision: 3,
default: () => 'CURRENT_TIMESTAMP(3)',
})
balanceDate: Date
@ManyToMany(() => Community, (community) => community.communityAccounts)
@JoinTable({
name: 'accounts_communities',
joinColumn: { name: 'account_id', referencedColumnName: 'id' },
inverseJoinColumn: { name: 'community_id', referencedColumnName: 'id' },
})
accountCommunities: Community[]
@OneToMany(() => AccountCommunity, (accountCommunity) => accountCommunity.account)
@JoinColumn({ name: 'account_id' })
accountCommunities: AccountCommunity[]
@OneToMany(() => TransactionRecipe, (recipe) => recipe.signingAccount)
transactionRecipesSigning?: TransactionRecipe[]

View File

@ -1,10 +1,10 @@
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm'
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, BaseEntity } from 'typeorm'
import { Account } from './Account'
import { Community } from './Community'
import { Account } from '../Account'
import { Community } from '../Community'
@Entity('accounts_communities')
export class AccountCommunity {
export class AccountCommunity extends BaseEntity {
@PrimaryGeneratedColumn('increment', { unsigned: true })
id: number
@ -15,16 +15,16 @@ export class AccountCommunity {
@Column({ name: 'account_id', type: 'int', unsigned: true })
accountId: number
@ManyToOne(() => Community, (community) => community.communityAccounts)
@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' })
@Column({ name: 'valid_from', type: 'datetime', precision: 3 })
validFrom: Date
@Column({ name: 'valid_to', type: 'datetime', nullable: true })
@Column({ name: 'valid_to', type: 'datetime', precision: 3, nullable: true })
validTo?: Date
}

View File

@ -2,18 +2,17 @@ import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
JoinColumn,
OneToOne,
OneToMany,
ManyToMany,
JoinTable,
BaseEntity,
} from 'typeorm'
import { Account } from './Account'
import { TransactionRecipe } from './TransactionRecipe'
import { Account } from '../Account'
import { TransactionRecipe } from '../TransactionRecipe'
import { AccountCommunity } from '../AccountCommunity'
@Entity('communities')
export class Community {
export class Community extends BaseEntity {
@PrimaryGeneratedColumn('increment', { unsigned: true })
id: number
@ -46,19 +45,20 @@ export class Community {
@JoinColumn({ name: 'auf_account_id' })
aufAccount?: Account
@CreateDateColumn({ name: 'created_at', type: 'datetime', default: () => 'CURRENT_TIMESTAMP(3)' })
@Column({
name: 'created_at',
type: 'datetime',
precision: 3,
default: () => 'CURRENT_TIMESTAMP(3)',
})
createdAt: Date
@Column({ name: 'confirmed_at', type: 'datetime', nullable: true })
@Column({ name: 'confirmed_at', type: 'datetime', precision: 3, nullable: true })
confirmedAt?: Date
@ManyToMany(() => Account, (account) => account.accountCommunities)
@JoinTable({
name: 'accounts_communities',
joinColumn: { name: 'community_id', referencedColumnName: 'id' },
inverseJoinColumn: { name: 'account_id', referencedColumnName: 'id' },
})
communityAccounts: Account[]
@OneToMany(() => AccountCommunity, (accountCommunity) => accountCommunity.community)
@JoinColumn({ name: 'community_id' })
accountCommunities: AccountCommunity[]
@OneToMany(() => TransactionRecipe, (recipe) => recipe.senderCommunity)
transactionRecipesSender?: TransactionRecipe[]

View File

@ -1,12 +1,20 @@
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, OneToOne } from 'typeorm'
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'
import { Account } from '../Account'
import { TransactionRecipe } from '../TransactionRecipe'
@Entity('confirmed_transactions')
export class ConfirmedTransaction {
export class ConfirmedTransaction extends BaseEntity {
@PrimaryGeneratedColumn('increment', { unsigned: true, type: 'bigint' })
id: number
@ -44,6 +52,6 @@ export class ConfirmedTransaction {
@Column({ name: 'iota_milestone', type: 'bigint' })
iotaMilestone: number
@Column({ name: 'confirmed_at', type: 'datetime' })
@Column({ name: 'confirmed_at', type: 'datetime', precision: 3 })
confirmedAt: Date
}

View File

@ -1,7 +1,7 @@
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'
import { Entity, PrimaryGeneratedColumn, Column, BaseEntity } from 'typeorm'
@Entity('invalid_transactions')
export class InvalidTransaction {
export class InvalidTransaction extends BaseEntity {
@PrimaryGeneratedColumn('increment', { unsigned: true, type: 'bigint' })
id: number

View 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
}

View File

@ -2,20 +2,20 @@ import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
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'
import { Account } from '../Account'
import { Community } from '../Community'
import { ConfirmedTransaction } from '../ConfirmedTransaction'
@Entity('transaction_recipes')
export class TransactionRecipe {
export class TransactionRecipe extends BaseEntity {
@PrimaryGeneratedColumn('increment', { unsigned: true, type: 'bigint' })
id: number
@ -48,7 +48,7 @@ export class TransactionRecipe {
@JoinColumn({ name: 'recipient_community_id' })
recipientCommunity?: Community
@Column({ name: 'sender_community_id', type: 'int', unsigned: true, nullable: true })
@Column({ name: 'recipient_community_id', type: 'int', unsigned: true, nullable: true })
recipientCommunityId?: number
@Column({
@ -63,7 +63,7 @@ export class TransactionRecipe {
@Column({ type: 'tinyint' })
type: number
@CreateDateColumn({ name: 'created_at', type: 'datetime' })
@Column({ name: 'created_at', type: 'datetime', precision: 3 })
createdAt: Date
@Column({ name: 'body_bytes', type: 'blob' })

View File

@ -1,14 +1,6 @@
import {
BaseEntity,
Entity,
PrimaryGeneratedColumn,
Column,
OneToMany,
JoinColumn,
CreateDateColumn,
} from 'typeorm'
import { BaseEntity, Entity, PrimaryGeneratedColumn, Column, OneToMany, JoinColumn } from 'typeorm'
import { Account } from './Account'
import { Account } from '../Account'
@Entity('users', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' })
export class User extends BaseEntity {
@ -26,9 +18,10 @@ export class User extends BaseEntity {
@Column({ name: 'derive1_pubkey', type: 'binary', length: 32, unique: true })
derive1Pubkey: Buffer
@CreateDateColumn({
@Column({
name: 'created_at',
type: 'datetime',
precision: 3,
default: () => 'CURRENT_TIMESTAMP(3)',
})
createdAt: Date
@ -36,6 +29,7 @@ export class User extends BaseEntity {
@Column({
name: 'confirmed_at',
type: 'datetime',
precision: 3,
nullable: true,
})
confirmedAt?: Date

View 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[]
}

View 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' })
validFrom: Date
@Column({ name: 'valid_to', type: 'datetime', nullable: true })
validTo?: Date
}

View 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[]
}

View 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' })
confirmedAt: Date
}

View 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[]
}

View File

@ -1 +1 @@
export { Account } from './0001-init_db/Account'
export { Account } from './0002-refactor_add_community/Account'

View File

@ -1 +1 @@
export { AccountCommunity } from './0001-init_db/AccountCommunity'
export { AccountCommunity } from './0002-refactor_add_community/AccountCommunity'

View File

@ -1 +1 @@
export { Community } from './0001-init_db/Community'
export { Community } from './0002-refactor_add_community/Community'

View File

@ -1 +1 @@
export { ConfirmedTransaction } from './0001-init_db/ConfirmedTransaction'
export { ConfirmedTransaction } from './0002-refactor_add_community/ConfirmedTransaction'

View File

@ -0,0 +1 @@
export { Migration } from './0001-init_db/Migration'

View File

@ -1 +1 @@
export { User } from './0001-init_db/User'
export { User } from './0002-refactor_add_community/User'

View 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,
]

View File

@ -125,6 +125,6 @@ export async function downgrade(queryFn: (query: string, values?: any[]) => Prom
await queryFn(`DROP TABLE IF EXISTS \`accounts_communities\`;`)
await queryFn(`DROP TABLE IF EXISTS \`transaction_recipes\`;`)
await queryFn(`DROP TABLE IF EXISTS \`confirmed_transactions\`;`)
await queryFn(`DROP TABLE IF EXISTS \`community\`;`)
await queryFn(`DROP TABLE IF EXISTS \`communities\`;`)
await queryFn(`DROP TABLE IF EXISTS \`invalid_transactions\`;`)
}

View File

@ -0,0 +1,61 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
/* eslint-disable @typescript-eslint/no-explicit-any */
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
// write upgrade logic as parameter of queryFn
await queryFn(
`ALTER TABLE \`communities\` MODIFY COLUMN \`root_privkey\` binary(64) NULL DEFAULT NULL;`,
)
await queryFn(
`ALTER TABLE \`communities\` MODIFY COLUMN \`root_pubkey\` binary(32) NULL DEFAULT NULL;`,
)
await queryFn(
`ALTER TABLE \`communities\` MODIFY COLUMN \`root_chaincode\` binary(32) NULL DEFAULT NULL;`,
)
await queryFn(
`ALTER TABLE \`communities\` MODIFY COLUMN \`confirmed_at\` datetime NULL DEFAULT NULL;`,
)
await queryFn(`ALTER TABLE \`users\` MODIFY COLUMN \`confirmed_at\` datetime NULL DEFAULT NULL;`)
await queryFn(
`ALTER TABLE \`accounts\` MODIFY COLUMN \`confirmed_at\` datetime NULL DEFAULT NULL;`,
)
await queryFn(
`ALTER TABLE \`accounts\` MODIFY COLUMN \`balance_date\` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP;`,
)
await queryFn(
`ALTER TABLE \`accounts_communities\` MODIFY COLUMN \`valid_from\` datetime NOT NULL;`,
)
await queryFn(
`ALTER TABLE \`accounts_communities\` MODIFY COLUMN \`valid_to\` datetime NULL DEFAULT NULL;`,
)
await queryFn(
`ALTER TABLE \`confirmed_transactions\` MODIFY COLUMN \`confirmed_at\` datetime NOT NULL;`,
)
}
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
await queryFn(
`ALTER TABLE \`communities\` MODIFY COLUMN \`root_privkey\` binary(32) DEFAULT NULL;`,
)
await queryFn(`ALTER TABLE \`communities\` MODIFY COLUMN \`root_pubkey\` binary(32) NOT NULL;`)
await queryFn(
`ALTER TABLE \`communities\` MODIFY COLUMN \`root_chaincode\` binary(32) DEFAULT NULL;`,
)
await queryFn(
`ALTER TABLE \`communities\` MODIFY COLUMN \`confirmed_at\` datetime(3) DEFAULT NULL;`,
)
await queryFn(`ALTER TABLE \`users\` MODIFY COLUMN \`confirmed_at\` datetime(3) DEFAULT NULL;`)
await queryFn(`ALTER TABLE \`accounts\` MODIFY COLUMN \`confirmed_at\` datetime(3) DEFAULT NULL;`)
await queryFn(
`ALTER TABLE \`accounts\` MODIFY COLUMN \`balance_date\` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3);`,
)
await queryFn(
`ALTER TABLE \`accounts_communities\` MODIFY COLUMN \`valid_from\` datetime(3) NOT NULL;`,
)
await queryFn(
`ALTER TABLE \`accounts_communities\` MODIFY COLUMN \`valid_to\` datetime(3) DEFAULT NULL;`,
)
await queryFn(
`ALTER TABLE \`confirmed_transactions\` MODIFY COLUMN \`confirmed_at\` datetime(3) NOT NULL;`,
)
}

View File

@ -103,6 +103,7 @@ services:
- dlt_connector_modules:/app/node_modules
# bind the local folder to the docker to allow live reload
- ./dlt-connector:/app
- ./dlt-database:/dlt-database
########################################################
# FEDERATION ###########################################

View File

@ -64,6 +64,7 @@ services:
- internal-net
environment:
- NODE_ENV="test"
- DB_HOST=mariadb
########################################################
# DATABASE #############################################

View File

@ -163,16 +163,17 @@ services:
- internal-net
- external-net
ports:
- 6000:6000
- 6010:6010
restart: always
environment:
# Envs used in Dockerfile
# - DOCKER_WORKDIR="/app"
- PORT=6000
- PORT=6010
- BUILD_DATE
- BUILD_VERSION
- BUILD_COMMIT
- NODE_ENV="production"
- DB_HOST=mariadb
# Application only envs
volumes:
# <host_machine_directory>:<container_directory> mirror bidirectional path in local context with path in Docker container

View File

@ -6,7 +6,7 @@ module.exports = {
collectCoverageFrom: ['src/**/*.ts', '!**/node_modules/**', '!src/seeds/**', '!build/**'],
coverageThreshold: {
global: {
lines: 74,
lines: 75,
},
},
setupFiles: ['<rootDir>/test/testSetup.ts'],

View File

@ -10,11 +10,8 @@ import { GraphQLError } from 'graphql'
import { cleanDB, testEnvironment } from '@test/helpers'
import { logger } from '@test/testSetup'
import { Connection } from '@dbTools/typeorm'
import { PendingTransaction as DbPendingTransaction } from '@entity/PendingTransaction'
import Decimal from 'decimal.js-light'
import { PendingTransactionState } from '../enum/PendingTransactionState'
import { TransactionTypeId } from '../enum/TransactionTypeId'
import { Transaction as DbTransaction } from '@entity/Transaction'
import { SendCoinsArgs } from '../model/SendCoinsArgs'
let mutate: ApolloServerTestClient['mutate'], con: Connection
// let query: ApolloServerTestClient['query']
@ -49,54 +46,24 @@ afterAll(async () => {
describe('SendCoinsResolver', () => {
const voteForSendCoinsMutation = `
mutation (
$recipientCommunityUuid: String!
$recipientUserIdentifier: String!
$creationDate: String!
$amount: Decimal!
$memo: String!
$senderCommunityUuid: String!
$senderUserUuid: String!
$senderUserName: String!
) {
voteForSendCoins(
recipientCommunityUuid: $recipientCommunityUuid
recipientUserIdentifier: $recipientUserIdentifier
creationDate: $creationDate
amount: $amount
memo: $memo
senderCommunityUuid: $senderCommunityUuid
senderUserUuid: $senderUserUuid
senderUserName: $senderUserName
) {
mutation ($args: SendCoinsArgs!) {
voteForSendCoins(data: $args) {
vote
recipGradidoID
recipName
}
}
`
}`
const settleSendCoinsMutation = `
mutation (
$recipientCommunityUuid: String!
$recipientUserIdentifier: String!
$creationDate: String!
$amount: Decimal!
$memo: String!
$senderCommunityUuid: String!
$senderUserUuid: String!
$senderUserName: String!
) {
settleSendCoins(
recipientCommunityUuid: $recipientCommunityUuid
recipientUserIdentifier: $recipientUserIdentifier
creationDate: $creationDate
amount: $amount
memo: $memo
senderCommunityUuid: $senderCommunityUuid
senderUserUuid: $senderUserUuid
senderUserName: $senderUserName
)
mutation ($args: SendCoinsArgs!) {
settleSendCoins(data: $args)
}`
const revertSendCoinsMutation = `
mutation ($args: SendCoinsArgs!) {
revertSendCoins(data: $args)
}`
const revertSettledSendCoinsMutation = `
mutation ($args: SendCoinsArgs!) {
revertSettledSendCoins(data: $args)
}`
beforeEach(async () => {
@ -154,19 +121,21 @@ describe('SendCoinsResolver', () => {
describe('unknown recipient community', () => {
it('throws an error', async () => {
jest.clearAllMocks()
const args = new SendCoinsArgs()
args.recipientCommunityUuid = 'invalid foreignCom'
args.recipientUserIdentifier = recipUser.gradidoID
args.creationDate = new Date().toISOString()
args.amount = new Decimal(100)
args.memo = 'X-Com-TX memo'
if (homeCom.communityUuid) {
args.senderCommunityUuid = homeCom.communityUuid
}
args.senderUserUuid = sendUser.gradidoID
args.senderUserName = fullName(sendUser.firstName, sendUser.lastName)
expect(
await mutate({
mutation: voteForSendCoinsMutation,
variables: {
recipientCommunityUuid: 'invalid foreignCom',
recipientUserIdentifier: recipUser.gradidoID,
creationDate: new Date().toISOString(),
amount: 100,
memo: 'X-Com-TX memo',
senderCommunityUuid: homeCom.communityUuid,
senderUserUuid: sendUser.gradidoID,
senderUserName: fullName(sendUser.firstName, sendUser.lastName),
},
variables: { args },
}),
).toEqual(
expect.objectContaining({
@ -179,19 +148,23 @@ describe('SendCoinsResolver', () => {
describe('unknown recipient user', () => {
it('throws an error', async () => {
jest.clearAllMocks()
const args = new SendCoinsArgs()
if (foreignCom.communityUuid) {
args.recipientCommunityUuid = foreignCom.communityUuid
}
args.recipientUserIdentifier = 'invalid recipient'
args.creationDate = new Date().toISOString()
args.amount = new Decimal(100)
args.memo = 'X-Com-TX memo'
if (homeCom.communityUuid) {
args.senderCommunityUuid = homeCom.communityUuid
}
args.senderUserUuid = sendUser.gradidoID
args.senderUserName = fullName(sendUser.firstName, sendUser.lastName)
expect(
await mutate({
mutation: voteForSendCoinsMutation,
variables: {
recipientCommunityUuid: foreignCom.communityUuid,
recipientUserIdentifier: 'invalid recipient',
creationDate: new Date().toISOString(),
amount: 100,
memo: 'X-Com-TX memo',
senderCommunityUuid: homeCom.communityUuid,
senderUserUuid: sendUser.gradidoID,
senderUserName: fullName(sendUser.firstName, sendUser.lastName),
},
variables: { args },
}),
).toEqual(
expect.objectContaining({
@ -208,19 +181,23 @@ describe('SendCoinsResolver', () => {
describe('valid X-Com-TX voted', () => {
it('throws an error', async () => {
jest.clearAllMocks()
const args = new SendCoinsArgs()
if (foreignCom.communityUuid) {
args.recipientCommunityUuid = foreignCom.communityUuid
}
args.recipientUserIdentifier = recipUser.gradidoID
args.creationDate = new Date().toISOString()
args.amount = new Decimal(100)
args.memo = 'X-Com-TX memo'
if (homeCom.communityUuid) {
args.senderCommunityUuid = homeCom.communityUuid
}
args.senderUserUuid = sendUser.gradidoID
args.senderUserName = fullName(sendUser.firstName, sendUser.lastName)
expect(
await mutate({
mutation: voteForSendCoinsMutation,
variables: {
recipientCommunityUuid: foreignCom.communityUuid,
recipientUserIdentifier: recipUser.gradidoID,
creationDate: new Date().toISOString(),
amount: 100,
memo: 'X-Com-TX memo',
senderCommunityUuid: homeCom.communityUuid,
senderUserUuid: sendUser.gradidoID,
senderUserName: fullName(sendUser.firstName, sendUser.lastName),
},
variables: { args },
}),
).toEqual(
expect.objectContaining({
@ -238,63 +215,46 @@ describe('SendCoinsResolver', () => {
})
describe('revertSendCoins', () => {
const revertSendCoinsMutation = `
mutation (
$recipientCommunityUuid: String!
$recipientUserIdentifier: String!
$creationDate: String!
$amount: Decimal!
$memo: String!
$senderCommunityUuid: String!
$senderUserUuid: String!
$senderUserName: String!
) {
revertSendCoins(
recipientCommunityUuid: $recipientCommunityUuid
recipientUserIdentifier: $recipientUserIdentifier
creationDate: $creationDate
amount: $amount
memo: $memo
senderCommunityUuid: $senderCommunityUuid
senderUserUuid: $senderUserUuid
senderUserName: $senderUserName
)
}`
const creationDate = new Date()
beforeEach(async () => {
const args = new SendCoinsArgs()
if (foreignCom.communityUuid) {
args.recipientCommunityUuid = foreignCom.communityUuid
}
args.recipientUserIdentifier = recipUser.gradidoID
args.creationDate = creationDate.toISOString()
args.amount = new Decimal(100)
args.memo = 'X-Com-TX memo'
if (homeCom.communityUuid) {
args.senderCommunityUuid = homeCom.communityUuid
}
args.senderUserUuid = sendUser.gradidoID
args.senderUserName = fullName(sendUser.firstName, sendUser.lastName)
await mutate({
mutation: voteForSendCoinsMutation,
variables: {
recipientCommunityUuid: foreignCom.communityUuid,
recipientUserIdentifier: recipUser.gradidoID,
creationDate: creationDate.toISOString(),
amount: 100,
memo: 'X-Com-TX memo',
senderCommunityUuid: homeCom.communityUuid,
senderUserUuid: sendUser.gradidoID,
senderUserName: fullName(sendUser.firstName, sendUser.lastName),
},
variables: { args },
})
})
describe('unknown recipient community', () => {
it('throws an error', async () => {
jest.clearAllMocks()
const args = new SendCoinsArgs()
args.recipientCommunityUuid = 'invalid foreignCom'
args.recipientUserIdentifier = recipUser.gradidoID
args.creationDate = creationDate.toISOString()
args.amount = new Decimal(100)
args.memo = 'X-Com-TX memo'
if (homeCom.communityUuid) {
args.senderCommunityUuid = homeCom.communityUuid
}
args.senderUserUuid = sendUser.gradidoID
args.senderUserName = fullName(sendUser.firstName, sendUser.lastName)
expect(
await mutate({
mutation: revertSendCoinsMutation,
variables: {
recipientCommunityUuid: 'invalid foreignCom',
recipientUserIdentifier: recipUser.gradidoID,
creationDate: creationDate.toISOString(),
amount: 100,
memo: 'X-Com-TX memo',
senderCommunityUuid: homeCom.communityUuid,
senderUserUuid: sendUser.gradidoID,
senderUserName: fullName(sendUser.firstName, sendUser.lastName),
},
variables: { args },
}),
).toEqual(
expect.objectContaining({
@ -307,19 +267,23 @@ describe('SendCoinsResolver', () => {
describe('unknown recipient user', () => {
it('throws an error', async () => {
jest.clearAllMocks()
const args = new SendCoinsArgs()
if (foreignCom.communityUuid) {
args.recipientCommunityUuid = foreignCom.communityUuid
}
args.recipientUserIdentifier = 'invalid recipient'
args.creationDate = creationDate.toISOString()
args.amount = new Decimal(100)
args.memo = 'X-Com-TX memo'
if (homeCom.communityUuid) {
args.senderCommunityUuid = homeCom.communityUuid
}
args.senderUserUuid = sendUser.gradidoID
args.senderUserName = fullName(sendUser.firstName, sendUser.lastName)
expect(
await mutate({
mutation: revertSendCoinsMutation,
variables: {
recipientCommunityUuid: foreignCom.communityUuid,
recipientUserIdentifier: 'invalid recipient',
creationDate: creationDate.toISOString(),
amount: 100,
memo: 'X-Com-TX memo',
senderCommunityUuid: homeCom.communityUuid,
senderUserUuid: sendUser.gradidoID,
senderUserName: fullName(sendUser.firstName, sendUser.lastName),
},
variables: { args },
}),
).toEqual(
expect.objectContaining({
@ -336,19 +300,23 @@ describe('SendCoinsResolver', () => {
describe('valid X-Com-TX reverted', () => {
it('throws an error', async () => {
jest.clearAllMocks()
const args = new SendCoinsArgs()
if (foreignCom.communityUuid) {
args.recipientCommunityUuid = foreignCom.communityUuid
}
args.recipientUserIdentifier = recipUser.gradidoID
args.creationDate = creationDate.toISOString()
args.amount = new Decimal(100)
args.memo = 'X-Com-TX memo'
if (homeCom.communityUuid) {
args.senderCommunityUuid = homeCom.communityUuid
}
args.senderUserUuid = sendUser.gradidoID
args.senderUserName = fullName(sendUser.firstName, sendUser.lastName)
expect(
await mutate({
mutation: revertSendCoinsMutation,
variables: {
recipientCommunityUuid: foreignCom.communityUuid,
recipientUserIdentifier: recipUser.gradidoID,
creationDate: creationDate.toISOString(),
amount: 100,
memo: 'X-Com-TX memo',
senderCommunityUuid: homeCom.communityUuid,
senderUserUuid: sendUser.gradidoID,
senderUserName: fullName(sendUser.firstName, sendUser.lastName),
},
variables: { args },
}),
).toEqual(
expect.objectContaining({
@ -362,46 +330,46 @@ describe('SendCoinsResolver', () => {
})
describe('settleSendCoins', () => {
let pendingTx: DbPendingTransaction
const creationDate = new Date()
beforeEach(async () => {
pendingTx = DbPendingTransaction.create()
pendingTx.amount = new Decimal(100)
pendingTx.balanceDate = creationDate
// pendingTx.balance = new Decimal(0)
pendingTx.linkedUserId = sendUser.id
const args = new SendCoinsArgs()
if (foreignCom.communityUuid) {
pendingTx.linkedUserCommunityUuid = foreignCom.communityUuid
args.recipientCommunityUuid = foreignCom.communityUuid
}
pendingTx.linkedUserGradidoID = sendUser.gradidoID
pendingTx.state = PendingTransactionState.NEW
pendingTx.typeId = TransactionTypeId.RECEIVE
pendingTx.memo = 'X-Com-TX memo'
pendingTx.userId = recipUser.id
args.recipientUserIdentifier = recipUser.gradidoID
args.creationDate = creationDate.toISOString()
args.amount = new Decimal(100)
args.memo = 'X-Com-TX memo'
if (homeCom.communityUuid) {
pendingTx.userCommunityUuid = homeCom.communityUuid
args.senderCommunityUuid = homeCom.communityUuid
}
pendingTx.userGradidoID = recipUser.gradidoID
await DbPendingTransaction.insert(pendingTx)
args.senderUserUuid = sendUser.gradidoID
args.senderUserName = fullName(sendUser.firstName, sendUser.lastName)
await mutate({
mutation: voteForSendCoinsMutation,
variables: { args },
})
})
describe('unknown recipient community', () => {
it('throws an error', async () => {
jest.clearAllMocks()
const args = new SendCoinsArgs()
args.recipientCommunityUuid = 'invalid foreignCom'
args.recipientUserIdentifier = recipUser.gradidoID
args.creationDate = creationDate.toISOString()
args.amount = new Decimal(100)
args.memo = 'X-Com-TX memo'
if (homeCom.communityUuid) {
args.senderCommunityUuid = homeCom.communityUuid
}
args.senderUserUuid = sendUser.gradidoID
args.senderUserName = fullName(sendUser.firstName, sendUser.lastName)
expect(
await mutate({
mutation: settleSendCoinsMutation,
variables: {
recipientCommunityUuid: 'invalid foreignCom',
recipientUserIdentifier: recipUser.gradidoID,
creationDate: creationDate.toISOString(),
amount: 100,
memo: 'X-Com-TX memo',
senderCommunityUuid: foreignCom.communityUuid,
senderUserUuid: sendUser.gradidoID,
senderUserName: fullName(sendUser.firstName, sendUser.lastName),
},
variables: { args },
}),
).toEqual(
expect.objectContaining({
@ -414,19 +382,23 @@ describe('SendCoinsResolver', () => {
describe('unknown recipient user', () => {
it('throws an error', async () => {
jest.clearAllMocks()
const args = new SendCoinsArgs()
if (foreignCom.communityUuid) {
args.recipientCommunityUuid = foreignCom.communityUuid
}
args.recipientUserIdentifier = 'invalid recipient'
args.creationDate = creationDate.toISOString()
args.amount = new Decimal(100)
args.memo = 'X-Com-TX memo'
if (homeCom.communityUuid) {
args.senderCommunityUuid = homeCom.communityUuid
}
args.senderUserUuid = sendUser.gradidoID
args.senderUserName = fullName(sendUser.firstName, sendUser.lastName)
expect(
await mutate({
mutation: settleSendCoinsMutation,
variables: {
recipientCommunityUuid: homeCom.communityUuid,
recipientUserIdentifier: 'invalid recipient',
creationDate: creationDate.toISOString(),
amount: 100,
memo: 'X-Com-TX memo',
senderCommunityUuid: foreignCom.communityUuid,
senderUserUuid: sendUser.gradidoID,
senderUserName: fullName(sendUser.firstName, sendUser.lastName),
},
variables: { args },
}),
).toEqual(
expect.objectContaining({
@ -443,19 +415,23 @@ describe('SendCoinsResolver', () => {
describe('valid X-Com-TX settled', () => {
it('throws an error', async () => {
jest.clearAllMocks()
const args = new SendCoinsArgs()
if (foreignCom.communityUuid) {
args.recipientCommunityUuid = foreignCom.communityUuid
}
args.recipientUserIdentifier = recipUser.gradidoID
args.creationDate = creationDate.toISOString()
args.amount = new Decimal(100)
args.memo = 'X-Com-TX memo'
if (homeCom.communityUuid) {
args.senderCommunityUuid = homeCom.communityUuid
}
args.senderUserUuid = sendUser.gradidoID
args.senderUserName = fullName(sendUser.firstName, sendUser.lastName)
expect(
await mutate({
mutation: settleSendCoinsMutation,
variables: {
recipientCommunityUuid: homeCom.communityUuid,
recipientUserIdentifier: recipUser.gradidoID,
creationDate: creationDate.toISOString(),
amount: 100,
memo: 'X-Com-TX memo',
senderCommunityUuid: foreignCom.communityUuid,
senderUserUuid: sendUser.gradidoID,
senderUserName: fullName(sendUser.firstName, sendUser.lastName),
},
variables: { args },
}),
).toEqual(
expect.objectContaining({
@ -469,88 +445,50 @@ describe('SendCoinsResolver', () => {
})
describe('revertSettledSendCoins', () => {
const revertSettledSendCoinsMutation = `
mutation (
$recipientCommunityUuid: String!
$recipientUserIdentifier: String!
$creationDate: String!
$amount: Decimal!
$memo: String!
$senderCommunityUuid: String!
$senderUserUuid: String!
$senderUserName: String!
) {
revertSettledSendCoins(
recipientCommunityUuid: $recipientCommunityUuid
recipientUserIdentifier: $recipientUserIdentifier
creationDate: $creationDate
amount: $amount
memo: $memo
senderCommunityUuid: $senderCommunityUuid
senderUserUuid: $senderUserUuid
senderUserName: $senderUserName
)
}`
let pendingTx: DbPendingTransaction
let settledTx: DbTransaction
const creationDate = new Date()
beforeEach(async () => {
pendingTx = DbPendingTransaction.create()
pendingTx.amount = new Decimal(100)
pendingTx.balanceDate = creationDate
// pendingTx.balance = new Decimal(0)
pendingTx.linkedUserId = sendUser.id
const args = new SendCoinsArgs()
if (foreignCom.communityUuid) {
pendingTx.linkedUserCommunityUuid = foreignCom.communityUuid
args.recipientCommunityUuid = foreignCom.communityUuid
}
pendingTx.linkedUserGradidoID = sendUser.gradidoID
pendingTx.linkedUserName = fullName(sendUser.firstName, sendUser.lastName)
pendingTx.state = PendingTransactionState.SETTLED
pendingTx.typeId = TransactionTypeId.RECEIVE
pendingTx.memo = 'X-Com-TX memo'
pendingTx.userId = recipUser.id
args.recipientUserIdentifier = recipUser.gradidoID
args.creationDate = creationDate.toISOString()
args.amount = new Decimal(100)
args.memo = 'X-Com-TX memo'
if (homeCom.communityUuid) {
pendingTx.userCommunityUuid = homeCom.communityUuid
args.senderCommunityUuid = homeCom.communityUuid
}
pendingTx.userGradidoID = recipUser.gradidoID
await DbPendingTransaction.insert(pendingTx)
settledTx = DbTransaction.create()
settledTx.amount = new Decimal(100)
settledTx.balanceDate = creationDate
// pendingTx.balance = new Decimal(0)
settledTx.linkedUserId = sendUser.id
settledTx.linkedUserCommunityUuid = foreignCom.communityUuid
settledTx.linkedUserGradidoID = sendUser.gradidoID
settledTx.linkedUserName = fullName(sendUser.firstName, sendUser.lastName)
settledTx.typeId = TransactionTypeId.RECEIVE
settledTx.memo = 'X-Com-TX memo'
settledTx.userId = recipUser.id
if (homeCom.communityUuid) {
settledTx.userCommunityUuid = homeCom.communityUuid
}
settledTx.userGradidoID = recipUser.gradidoID
await DbTransaction.insert(settledTx)
args.senderUserUuid = sendUser.gradidoID
args.senderUserName = fullName(sendUser.firstName, sendUser.lastName)
await mutate({
mutation: voteForSendCoinsMutation,
variables: { args },
})
await mutate({
mutation: settleSendCoinsMutation,
variables: { args },
})
})
describe('unknown recipient community', () => {
it('throws an error', async () => {
jest.clearAllMocks()
const args = new SendCoinsArgs()
args.recipientCommunityUuid = 'invalid foreignCom'
args.recipientUserIdentifier = recipUser.gradidoID
args.creationDate = creationDate.toISOString()
args.amount = new Decimal(100)
args.memo = 'X-Com-TX memo'
if (homeCom.communityUuid) {
args.senderCommunityUuid = homeCom.communityUuid
}
args.senderUserUuid = sendUser.gradidoID
args.senderUserName = fullName(sendUser.firstName, sendUser.lastName)
expect(
await mutate({
mutation: revertSettledSendCoinsMutation,
variables: {
recipientCommunityUuid: 'invalid foreignCom',
recipientUserIdentifier: recipUser.gradidoID,
creationDate: creationDate.toISOString(),
amount: 100,
memo: 'X-Com-TX memo',
senderCommunityUuid: foreignCom.communityUuid,
senderUserUuid: sendUser.gradidoID,
senderUserName: fullName(sendUser.firstName, sendUser.lastName),
},
variables: { args },
}),
).toEqual(
expect.objectContaining({
@ -563,19 +501,23 @@ describe('SendCoinsResolver', () => {
describe('unknown recipient user', () => {
it('throws an error', async () => {
jest.clearAllMocks()
const args = new SendCoinsArgs()
if (foreignCom.communityUuid) {
args.recipientCommunityUuid = foreignCom.communityUuid
}
args.recipientUserIdentifier = 'invalid recipient'
args.creationDate = creationDate.toISOString()
args.amount = new Decimal(100)
args.memo = 'X-Com-TX memo'
if (homeCom.communityUuid) {
args.senderCommunityUuid = homeCom.communityUuid
}
args.senderUserUuid = sendUser.gradidoID
args.senderUserName = fullName(sendUser.firstName, sendUser.lastName)
expect(
await mutate({
mutation: revertSettledSendCoinsMutation,
variables: {
recipientCommunityUuid: homeCom.communityUuid,
recipientUserIdentifier: 'invalid recipient',
creationDate: creationDate.toISOString(),
amount: 100,
memo: 'X-Com-TX memo',
senderCommunityUuid: foreignCom.communityUuid,
senderUserUuid: sendUser.gradidoID,
senderUserName: fullName(sendUser.firstName, sendUser.lastName),
},
variables: { args },
}),
).toEqual(
expect.objectContaining({
@ -592,19 +534,23 @@ describe('SendCoinsResolver', () => {
describe('valid X-Com-TX settled', () => {
it('throws an error', async () => {
jest.clearAllMocks()
const args = new SendCoinsArgs()
if (foreignCom.communityUuid) {
args.recipientCommunityUuid = foreignCom.communityUuid
}
args.recipientUserIdentifier = recipUser.gradidoID
args.creationDate = creationDate.toISOString()
args.amount = new Decimal(100)
args.memo = 'X-Com-TX memo'
if (homeCom.communityUuid) {
args.senderCommunityUuid = homeCom.communityUuid
}
args.senderUserUuid = sendUser.gradidoID
args.senderUserName = fullName(sendUser.firstName, sendUser.lastName)
expect(
await mutate({
mutation: revertSettledSendCoinsMutation,
variables: {
recipientCommunityUuid: homeCom.communityUuid,
recipientUserIdentifier: recipUser.gradidoID,
creationDate: creationDate.toISOString(),
amount: 100,
memo: 'X-Com-TX memo',
senderCommunityUuid: foreignCom.communityUuid,
senderUserUuid: sendUser.gradidoID,
senderUserName: fullName(sendUser.firstName, sendUser.lastName),
},
variables: { args },
}),
).toEqual(
expect.objectContaining({

View File

@ -157,8 +157,8 @@ export class SendCoinsResolver {
} else {
logger.debug(
'XCom: revertSendCoins NOT matching pendingTX for remove:',
pendingTx?.amount,
args.amount,
pendingTx?.amount.toString(),
args.amount.toString(),
)
throw new LogError(
`Can't find in revertSendCoins the pending receiver TX for args=`,
@ -283,7 +283,7 @@ export class SendCoinsResolver {
}
const pendingTx = await DbPendingTransaction.findOneBy({
userCommunityUuid: args.recipientCommunityUuid,
userGradidoID: receiverUser.gradidoID,
userGradidoID: args.recipientUserIdentifier,
state: PendingTransactionState.SETTLED,
typeId: TransactionTypeId.RECEIVE,
balanceDate: new Date(args.creationDate),

View File

@ -60,14 +60,31 @@ export async function revertSettledReceiveTransaction(
}
const lastTransaction = await getLastTransaction(receiverUser.id)
logger.debug(`LastTransaction vs PendingTransaction`)
logger.debug(`balance:`, lastTransaction?.balance.toString(), pendingTx.balance.toString())
logger.debug(
`balanceDate:`,
lastTransaction?.balanceDate.toISOString(),
pendingTx.balanceDate.toISOString(),
)
logger.debug(`GradidoID:`, lastTransaction?.userGradidoID, pendingTx.userGradidoID)
logger.debug(`Name:`, lastTransaction?.userName, pendingTx.userName)
logger.debug(`amount:`, lastTransaction?.amount.toString(), pendingTx.amount.toString())
logger.debug(`memo:`, lastTransaction?.memo, pendingTx.memo)
logger.debug(
`linkedUserGradidoID:`,
lastTransaction?.linkedUserGradidoID,
pendingTx.linkedUserGradidoID,
)
logger.debug(`linkedUserName:`, lastTransaction?.linkedUserName, pendingTx.linkedUserName)
// now the last Tx must be the equivalant to the pendingTX
if (
lastTransaction &&
lastTransaction.balance === pendingTx.balance &&
lastTransaction.balance.comparedTo(pendingTx.balance) === 0 &&
lastTransaction.balanceDate.toISOString() === pendingTx.balanceDate.toISOString() &&
lastTransaction.userGradidoID === pendingTx.userGradidoID &&
lastTransaction.userName === pendingTx.userName &&
lastTransaction.amount.toString() === pendingTx.amount.toString() &&
lastTransaction.amount.comparedTo(pendingTx.amount) === 0 &&
lastTransaction.memo === pendingTx.memo &&
lastTransaction.linkedUserGradidoID === pendingTx.linkedUserGradidoID &&
lastTransaction.linkedUserName === pendingTx.linkedUserName

View File

@ -1,20 +0,0 @@
import { Decimal } from 'decimal.js-light'
import { getLastTransaction } from './getLastTransaction'
import { calculateDecay } from './decay'
import { Decay } from '../api/1_0/model/Decay'
export async function calculateRecipientBalance(
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 }
}

View File

@ -7,7 +7,7 @@ export async function checkTradingLevel(homeCom: DbCommunity, amount: Decimal):
const tradingLevel = CONFIG.FEDERATION_TRADING_LEVEL
if (homeCom.url !== tradingLevel.RECEIVER_COMMUNITY_URL) {
logger.warn(
`X-Com: tradingLevel allows to receive coins only wiht url ${tradingLevel.RECEIVER_COMMUNITY_URL}`,
`X-Com: tradingLevel allows to receive coins only with url ${tradingLevel.RECEIVER_COMMUNITY_URL}`,
)
return false
}