mirror of
https://github.com/IT4Change/gradido.git
synced 2026-02-06 09:56:05 +00:00
Merge pull request #3223 from gradido/dlt_connector_try_dci
refactor(dlt): dlt connector try out dci
This commit is contained in:
commit
f4169db9b9
@ -16,6 +16,7 @@ module.exports = {
|
||||
moduleNameMapper: {
|
||||
'@/(.*)': '<rootDir>/src/$1',
|
||||
'@arg/(.*)': '<rootDir>/src/graphql/arg/$1',
|
||||
'@dltConnector/(.*)': '<rootDir>/src/apis/dltConnector/$1',
|
||||
'@enum/(.*)': '<rootDir>/src/graphql/enum/$1',
|
||||
'@model/(.*)': '<rootDir>/src/graphql/model/$1',
|
||||
'@union/(.*)': '<rootDir>/src/graphql/union/$1',
|
||||
|
||||
@ -25,8 +25,6 @@ let testEnv: {
|
||||
jest.mock('graphql-request', () => {
|
||||
const originalModule = jest.requireActual('graphql-request')
|
||||
|
||||
let testCursor = 0
|
||||
|
||||
return {
|
||||
__esModule: true,
|
||||
...originalModule,
|
||||
@ -38,30 +36,11 @@ jest.mock('graphql-request', () => {
|
||||
// why not using mockResolvedValueOnce or mockReturnValueOnce?
|
||||
// I have tried, but it didn't work and return every time the first value
|
||||
request: jest.fn().mockImplementation(() => {
|
||||
testCursor++
|
||||
if (testCursor === 4) {
|
||||
return Promise.resolve(
|
||||
// invalid, is 33 Bytes long as binary
|
||||
{
|
||||
transmitTransaction: {
|
||||
dltTransactionIdHex:
|
||||
'723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc516212A',
|
||||
},
|
||||
},
|
||||
)
|
||||
} else if (testCursor === 5) {
|
||||
throw Error('Connection error')
|
||||
} else {
|
||||
return Promise.resolve(
|
||||
// valid, is 32 Bytes long as binary
|
||||
{
|
||||
transmitTransaction: {
|
||||
dltTransactionIdHex:
|
||||
'723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc51621',
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
return Promise.resolve({
|
||||
transmitTransaction: {
|
||||
succeed: true,
|
||||
},
|
||||
})
|
||||
}),
|
||||
}
|
||||
}),
|
||||
@ -6,6 +6,9 @@ import { TransactionTypeId } from '@/graphql/enum/TransactionTypeId'
|
||||
import { LogError } from '@/server/LogError'
|
||||
import { backendLogger as logger } from '@/server/logger'
|
||||
|
||||
import { TransactionResult } from './model/TransactionResult'
|
||||
import { UserIdentifier } from './model/UserIdentifier'
|
||||
|
||||
const sendTransaction = gql`
|
||||
mutation ($input: TransactionInput!) {
|
||||
sendTransaction(data: $input) {
|
||||
@ -78,32 +81,42 @@ 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,
|
||||
senderCommunityUuid?: string,
|
||||
recipientCommunityUuid?: string,
|
||||
): Promise<string> {
|
||||
public async transmitTransaction(transaction: DbTransaction): Promise<boolean> {
|
||||
const typeString = getTransactionTypeString(transaction.typeId)
|
||||
const amountString = transaction.amount.toString()
|
||||
// no negative values in dlt connector, gradido concept don't use negative values so the code don't use it too
|
||||
const amountString = transaction.amount.abs().toString()
|
||||
const params = {
|
||||
input: {
|
||||
user: {
|
||||
uuid: transaction.userGradidoID,
|
||||
communityUuid: transaction.userCommunityUuid,
|
||||
} as UserIdentifier,
|
||||
linkedUser: {
|
||||
uuid: transaction.linkedUserGradidoID,
|
||||
communityUuid: transaction.linkedUserCommunityUuid,
|
||||
} as UserIdentifier,
|
||||
amount: amountString,
|
||||
type: typeString,
|
||||
createdAt: transaction.balanceDate.toISOString(),
|
||||
backendTransactionId: transaction.id,
|
||||
targetDate: transaction.creationDate?.toISOString(),
|
||||
},
|
||||
}
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const { data } = await this.client.rawRequest(sendTransaction, {
|
||||
input: {
|
||||
senderUser: {
|
||||
uuid: transaction.userGradidoID,
|
||||
communityUuid: senderCommunityUuid,
|
||||
},
|
||||
recipientUser: {
|
||||
uuid: transaction.linkedUserGradidoID,
|
||||
communityUuid: recipientCommunityUuid,
|
||||
},
|
||||
amount: amountString,
|
||||
type: typeString,
|
||||
createdAt: transaction.balanceDate.toString(),
|
||||
// TODO: add account nr for user after they have also more than one account in backend
|
||||
logger.debug('transmit transaction to dlt connector', params)
|
||||
const {
|
||||
data: {
|
||||
sendTransaction: { error, succeed },
|
||||
},
|
||||
})
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access
|
||||
return data.sendTransaction.dltTransactionIdHex
|
||||
} = await this.client.rawRequest<{ sendTransaction: TransactionResult }>(
|
||||
sendTransaction,
|
||||
params,
|
||||
)
|
||||
if (error) {
|
||||
throw new Error(error.message)
|
||||
}
|
||||
return succeed
|
||||
} catch (e) {
|
||||
throw new LogError('Error send sending transaction to dlt-connector: ', e)
|
||||
}
|
||||
14
backend/src/apis/dltConnector/enum/TransactionErrorType.ts
Normal file
14
backend/src/apis/dltConnector/enum/TransactionErrorType.ts
Normal file
@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Error Types for dlt-connector graphql responses
|
||||
*/
|
||||
export enum TransactionErrorType {
|
||||
NOT_IMPLEMENTED_YET = 'Not Implemented yet',
|
||||
MISSING_PARAMETER = 'Missing parameter',
|
||||
ALREADY_EXIST = 'Already exist',
|
||||
DB_ERROR = 'DB Error',
|
||||
PROTO_DECODE_ERROR = 'Proto Decode Error',
|
||||
PROTO_ENCODE_ERROR = 'Proto Encode Error',
|
||||
INVALID_SIGNATURE = 'Invalid Signature',
|
||||
LOGIC_ERROR = 'Logic Error',
|
||||
NOT_FOUND = 'Not found',
|
||||
}
|
||||
11
backend/src/apis/dltConnector/enum/TransactionType.ts
Normal file
11
backend/src/apis/dltConnector/enum/TransactionType.ts
Normal file
@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Transaction Types on Blockchain
|
||||
*/
|
||||
export enum TransactionType {
|
||||
GRADIDO_TRANSFER = 1,
|
||||
GRADIDO_CREATION = 2,
|
||||
GROUP_FRIENDS_UPDATE = 3,
|
||||
REGISTER_ADDRESS = 4,
|
||||
GRADIDO_DEFERRED_TRANSFER = 5,
|
||||
COMMUNITY_ROOT = 6,
|
||||
}
|
||||
7
backend/src/apis/dltConnector/model/TransactionError.ts
Normal file
7
backend/src/apis/dltConnector/model/TransactionError.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { TransactionErrorType } from '@dltConnector/enum/TransactionErrorType'
|
||||
|
||||
export interface TransactionError {
|
||||
type: TransactionErrorType
|
||||
message: string
|
||||
name: string
|
||||
}
|
||||
8
backend/src/apis/dltConnector/model/TransactionRecipe.ts
Normal file
8
backend/src/apis/dltConnector/model/TransactionRecipe.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { TransactionType } from '@dltConnector/enum/TransactionType'
|
||||
|
||||
export interface TransactionRecipe {
|
||||
id: number
|
||||
createdAt: string
|
||||
type: TransactionType
|
||||
topic: string
|
||||
}
|
||||
8
backend/src/apis/dltConnector/model/TransactionResult.ts
Normal file
8
backend/src/apis/dltConnector/model/TransactionResult.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { TransactionError } from './TransactionError'
|
||||
import { TransactionRecipe } from './TransactionRecipe'
|
||||
|
||||
export interface TransactionResult {
|
||||
error?: TransactionError
|
||||
recipe?: TransactionRecipe
|
||||
succeed: boolean
|
||||
}
|
||||
5
backend/src/apis/dltConnector/model/UserIdentifier.ts
Normal file
5
backend/src/apis/dltConnector/model/UserIdentifier.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export interface UserIdentifier {
|
||||
uuid: string
|
||||
communityUuid: string
|
||||
accountNr?: number
|
||||
}
|
||||
@ -22,6 +22,13 @@ import { logger, i18n as localization } from '@test/testSetup'
|
||||
|
||||
import { CONFIG } from '@/config'
|
||||
import { TransactionTypeId } from '@/graphql/enum/TransactionTypeId'
|
||||
import { creations } from '@/seeds/creation'
|
||||
import { creationFactory } from '@/seeds/factory/creation'
|
||||
import { userFactory } from '@/seeds/factory/user'
|
||||
import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg'
|
||||
import { bobBaumeister } from '@/seeds/users/bob-baumeister'
|
||||
import { peterLustig } from '@/seeds/users/peter-lustig'
|
||||
import { raeuberHotzenplotz } from '@/seeds/users/raeuber-hotzenplotz'
|
||||
|
||||
import { sendTransactionsToDltConnector } from './sendTransactionsToDltConnector'
|
||||
|
||||
@ -423,9 +430,17 @@ describe('create and send Transactions to DltConnector', () => {
|
||||
|
||||
describe('with 3 creations and active dlt-connector', () => {
|
||||
it('found 3 dlt-transactions', async () => {
|
||||
txCREATION1 = await createTxCREATION1(false)
|
||||
txCREATION2 = await createTxCREATION2(false)
|
||||
txCREATION3 = await createTxCREATION3(false)
|
||||
await userFactory(testEnv, bibiBloxberg)
|
||||
await userFactory(testEnv, peterLustig)
|
||||
await userFactory(testEnv, raeuberHotzenplotz)
|
||||
await userFactory(testEnv, bobBaumeister)
|
||||
let count = 0
|
||||
for (const creation of creations) {
|
||||
await creationFactory(testEnv, creation)
|
||||
count++
|
||||
// we need only 3 for testing
|
||||
if (count >= 3) break
|
||||
}
|
||||
await createHomeCommunity()
|
||||
|
||||
CONFIG.DLT_CONNECTOR = true
|
||||
@ -435,10 +450,7 @@ describe('create and send Transactions to DltConnector', () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return {
|
||||
data: {
|
||||
sendTransaction: {
|
||||
dltTransactionIdHex:
|
||||
'723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc51621',
|
||||
},
|
||||
sendTransaction: { succeed: true },
|
||||
},
|
||||
} as Response<unknown>
|
||||
})
|
||||
@ -464,7 +476,7 @@ describe('create and send Transactions to DltConnector', () => {
|
||||
expect.objectContaining({
|
||||
id: expect.any(Number),
|
||||
transactionId: transactions[0].id,
|
||||
messageId: '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc51621',
|
||||
messageId: 'sended',
|
||||
verified: false,
|
||||
createdAt: expect.any(Date),
|
||||
verifiedAt: null,
|
||||
@ -472,7 +484,7 @@ describe('create and send Transactions to DltConnector', () => {
|
||||
expect.objectContaining({
|
||||
id: expect.any(Number),
|
||||
transactionId: transactions[1].id,
|
||||
messageId: '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc51621',
|
||||
messageId: 'sended',
|
||||
verified: false,
|
||||
createdAt: expect.any(Date),
|
||||
verifiedAt: null,
|
||||
@ -480,7 +492,7 @@ describe('create and send Transactions to DltConnector', () => {
|
||||
expect.objectContaining({
|
||||
id: expect.any(Number),
|
||||
transactionId: transactions[2].id,
|
||||
messageId: '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc51621',
|
||||
messageId: 'sended',
|
||||
verified: false,
|
||||
createdAt: expect.any(Date),
|
||||
verifiedAt: null,
|
||||
@ -514,10 +526,7 @@ describe('create and send Transactions to DltConnector', () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return {
|
||||
data: {
|
||||
sendTransaction: {
|
||||
dltTransactionIdHex:
|
||||
'723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc51621',
|
||||
},
|
||||
sendTransaction: { succeed: true },
|
||||
},
|
||||
} as Response<unknown>
|
||||
})
|
||||
@ -569,7 +578,7 @@ describe('create and send Transactions to DltConnector', () => {
|
||||
expect.objectContaining({
|
||||
id: expect.any(Number),
|
||||
transactionId: txSEND1to2.id,
|
||||
messageId: '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc51621',
|
||||
messageId: 'sended',
|
||||
verified: false,
|
||||
createdAt: expect.any(Date),
|
||||
verifiedAt: null,
|
||||
@ -577,7 +586,7 @@ describe('create and send Transactions to DltConnector', () => {
|
||||
expect.objectContaining({
|
||||
id: expect.any(Number),
|
||||
transactionId: txRECEIVE2From1.id,
|
||||
messageId: '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc51621',
|
||||
messageId: 'sended',
|
||||
verified: false,
|
||||
createdAt: expect.any(Date),
|
||||
verifiedAt: null,
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import { IsNull } from '@dbTools/typeorm'
|
||||
import { Community } from '@entity/Community'
|
||||
import { DltTransaction } from '@entity/DltTransaction'
|
||||
import { Transaction } from '@entity/Transaction'
|
||||
|
||||
import { DltConnectorClient } from '@/apis/DltConnectorClient'
|
||||
import { DltConnectorClient } from '@dltConnector/DltConnectorClient'
|
||||
|
||||
import { backendLogger as logger } from '@/server/logger'
|
||||
import { Monitor, MonitorNames } from '@/util/Monitor'
|
||||
|
||||
@ -17,13 +17,6 @@ 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({
|
||||
@ -37,22 +30,14 @@ export async function sendTransactionsToDltConnector(): Promise<void> {
|
||||
continue
|
||||
}
|
||||
try {
|
||||
const messageId = await dltConnector.transmitTransaction(
|
||||
dltTx.transaction,
|
||||
senderCommunityUuid,
|
||||
recipientCommunityUuid,
|
||||
)
|
||||
const dltMessageId = Buffer.from(messageId, 'hex')
|
||||
if (dltMessageId.length !== 32) {
|
||||
logger.error(
|
||||
'Error dlt message id is invalid: %s, should by 32 Bytes long in binary after converting from hex',
|
||||
dltMessageId,
|
||||
)
|
||||
return
|
||||
const result = await dltConnector.transmitTransaction(dltTx.transaction)
|
||||
// message id isn't known at this point of time, because transaction will not direct sended to iota,
|
||||
// it will first go to db and then sended, if no transaction is in db before
|
||||
if (result) {
|
||||
dltTx.messageId = 'sended'
|
||||
await DltTransaction.save(dltTx)
|
||||
logger.info('store messageId=%s in dltTx=%d', dltTx.messageId, dltTx.id)
|
||||
}
|
||||
dltTx.messageId = dltMessageId.toString('hex')
|
||||
await DltTransaction.save(dltTx)
|
||||
logger.info('store messageId=%s in dltTx=%d', dltTx.messageId, dltTx.id)
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
`error while sending to dlt-connector or writing messageId of dltTx=${dltTx.id}`,
|
||||
|
||||
@ -49,6 +49,7 @@
|
||||
"paths": { /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
|
||||
"@/*": ["src/*"],
|
||||
"@arg/*": ["src/graphql/arg/*"],
|
||||
"@dltConnector/*": ["src/apis/dltConnector/*"],
|
||||
"@enum/*": ["src/graphql/enum/*"],
|
||||
"@model/*": ["src/graphql/model/*"],
|
||||
"@union/*": ["src/graphql/union/*"],
|
||||
|
||||
@ -14,6 +14,7 @@ module.exports = {
|
||||
// 'plugin:import/typescript',
|
||||
// 'plugin:security/recommended',
|
||||
'plugin:@eslint-community/eslint-comments/recommended',
|
||||
'plugin:dci-lint/recommended',
|
||||
],
|
||||
settings: {
|
||||
'import/parsers': {
|
||||
@ -36,6 +37,7 @@ module.exports = {
|
||||
htmlWhitespaceSensitivity: 'ignore',
|
||||
},
|
||||
],
|
||||
// 'dci-lint/literal-role-contracts': 'off'
|
||||
// import
|
||||
// 'import/export': 'error',
|
||||
// 'import/no-deprecated': 'error',
|
||||
@ -75,30 +77,30 @@ module.exports = {
|
||||
// 'import/no-named-default': 'error',
|
||||
// 'import/no-namespace': 'error',
|
||||
// 'import/no-unassigned-import': 'error',
|
||||
// 'import/order': [
|
||||
// 'error',
|
||||
// {
|
||||
// groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object', 'type'],
|
||||
// 'newlines-between': 'always',
|
||||
// pathGroups: [
|
||||
// {
|
||||
// pattern: '@?*/**',
|
||||
// group: 'external',
|
||||
// position: 'after',
|
||||
// },
|
||||
// {
|
||||
// pattern: '@/**',
|
||||
// group: 'external',
|
||||
// position: 'after',
|
||||
// },
|
||||
// ],
|
||||
// alphabetize: {
|
||||
// order: 'asc' /* sort in ascending order. Options: ['ignore', 'asc', 'desc'] */,
|
||||
// caseInsensitive: true /* ignore case. Options: [true, false] */,
|
||||
// },
|
||||
// distinctGroup: true,
|
||||
// },
|
||||
// ],
|
||||
'import/order': [
|
||||
'error',
|
||||
{
|
||||
groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object', 'type'],
|
||||
'newlines-between': 'always',
|
||||
pathGroups: [
|
||||
{
|
||||
pattern: '@?*/**',
|
||||
group: 'external',
|
||||
position: 'after',
|
||||
},
|
||||
{
|
||||
pattern: '@/**',
|
||||
group: 'external',
|
||||
position: 'after',
|
||||
},
|
||||
],
|
||||
alphabetize: {
|
||||
order: 'asc' /* sort in ascending order. Options: ['ignore', 'asc', 'desc'] */,
|
||||
caseInsensitive: true /* ignore case. Options: [true, false] */,
|
||||
},
|
||||
distinctGroup: true,
|
||||
},
|
||||
],
|
||||
// 'import/prefer-default-export': 'off',
|
||||
// n
|
||||
'n/handle-callback-err': 'error',
|
||||
|
||||
1
dlt-connector/@types/bip32-ed25519/index.d.ts
vendored
Normal file
1
dlt-connector/@types/bip32-ed25519/index.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
declare module 'bip32-ed25519'
|
||||
@ -6,7 +6,7 @@ module.exports = {
|
||||
collectCoverageFrom: ['src/**/*.ts', '!**/node_modules/**', '!src/seeds/**', '!build/**'],
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
lines: 77,
|
||||
lines: 71,
|
||||
},
|
||||
},
|
||||
setupFiles: ['<rootDir>/test/testSetup.ts'],
|
||||
@ -17,6 +17,7 @@ module.exports = {
|
||||
'@arg/(.*)': '<rootDir>/src/graphql/arg/$1',
|
||||
'@controller/(.*)': '<rootDir>/src/controller/$1',
|
||||
'@enum/(.*)': '<rootDir>/src/graphql/enum/$1',
|
||||
'@model/(.*)': '<rootDir>/src/graphql/model/$1',
|
||||
'@resolver/(.*)': '<rootDir>/src/graphql/resolver/$1',
|
||||
'@input/(.*)': '<rootDir>/src/graphql/input/$1',
|
||||
'@proto/(.*)': '<rootDir>/src/proto/$1',
|
||||
|
||||
@ -16,10 +16,11 @@
|
||||
"test": "cross-env TZ=UTC NODE_ENV=development jest --runInBand --forceExit --detectOpenHandles"
|
||||
},
|
||||
"dependencies": {
|
||||
"@apollo/protobufjs": "^1.2.7",
|
||||
"@apollo/server": "^4.7.5",
|
||||
"@apollo/utils.fetcher": "^3.0.0",
|
||||
"@iota/client": "^2.2.4",
|
||||
"bip32-ed25519": "^0.0.4",
|
||||
"bip39": "^3.1.0",
|
||||
"body-parser": "^1.20.2",
|
||||
"class-validator": "^0.14.0",
|
||||
"cors": "^2.8.5",
|
||||
@ -32,6 +33,7 @@
|
||||
"graphql-scalars": "^1.22.2",
|
||||
"log4js": "^6.7.1",
|
||||
"nodemon": "^2.0.20",
|
||||
"protobufjs": "^7.2.5",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"sodium-native": "^4.0.4",
|
||||
"tsconfig-paths": "^4.1.2",
|
||||
@ -51,6 +53,7 @@
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"eslint-config-standard": "^17.0.0",
|
||||
"eslint-import-resolver-typescript": "^3.5.4",
|
||||
"eslint-plugin-dci-lint": "^0.3.0",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"eslint-plugin-jest": "^27.2.1",
|
||||
"eslint-plugin-n": "^15.7.0",
|
||||
|
||||
@ -4,7 +4,7 @@ dotenv.config()
|
||||
|
||||
const constants = {
|
||||
LOG4JS_CONFIG: 'log4js-config.json',
|
||||
DB_VERSION: '0002-refactor_add_community',
|
||||
DB_VERSION: '0003-refactor_transaction_recipe',
|
||||
// default log level on production should be info
|
||||
LOG_LEVEL: process.env.LOG_LEVEL || 'info',
|
||||
CONFIG_VERSION: {
|
||||
|
||||
@ -1,66 +0,0 @@
|
||||
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'),
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1,28 +0,0 @@
|
||||
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)
|
||||
}
|
||||
@ -1,10 +0,0 @@
|
||||
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
|
||||
}
|
||||
@ -1,6 +0,0 @@
|
||||
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
|
||||
}
|
||||
@ -1,162 +0,0 @@
|
||||
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',
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1,74 +0,0 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
60
dlt-connector/src/data/Account.factory.ts
Normal file
60
dlt-connector/src/data/Account.factory.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import { Account } from '@entity/Account'
|
||||
import Decimal from 'decimal.js-light'
|
||||
|
||||
import { KeyPair } from '@/data/KeyPair'
|
||||
import { AddressType } from '@/data/proto/3_3/enum/AddressType'
|
||||
import { UserAccountDraft } from '@/graphql/input/UserAccountDraft'
|
||||
import { hardenDerivationIndex } from '@/utils/derivationHelper'
|
||||
import { accountTypeToAddressType } from '@/utils/typeConverter'
|
||||
|
||||
const GMW_ACCOUNT_DERIVATION_INDEX = 1
|
||||
const AUF_ACCOUNT_DERIVATION_INDEX = 2
|
||||
|
||||
export class AccountFactory {
|
||||
public static createAccount(
|
||||
createdAt: Date,
|
||||
derivationIndex: number,
|
||||
type: AddressType,
|
||||
parentKeyPair: KeyPair,
|
||||
): Account {
|
||||
const account = Account.create()
|
||||
account.derivationIndex = derivationIndex
|
||||
account.derive2Pubkey = parentKeyPair.derive([derivationIndex]).publicKey
|
||||
account.type = type.valueOf()
|
||||
account.createdAt = createdAt
|
||||
account.balanceOnConfirmation = new Decimal(0)
|
||||
account.balanceOnCreation = new Decimal(0)
|
||||
account.balanceCreatedAt = createdAt
|
||||
return account
|
||||
}
|
||||
|
||||
public static createAccountFromUserAccountDraft(
|
||||
{ createdAt, accountType, user }: UserAccountDraft,
|
||||
parentKeyPair: KeyPair,
|
||||
): Account {
|
||||
return AccountFactory.createAccount(
|
||||
new Date(createdAt),
|
||||
user.accountNr ?? 1,
|
||||
accountTypeToAddressType(accountType),
|
||||
parentKeyPair,
|
||||
)
|
||||
}
|
||||
|
||||
public static createGmwAccount(keyPair: KeyPair, createdAt: Date): Account {
|
||||
return AccountFactory.createAccount(
|
||||
createdAt,
|
||||
hardenDerivationIndex(GMW_ACCOUNT_DERIVATION_INDEX),
|
||||
AddressType.COMMUNITY_GMW,
|
||||
keyPair,
|
||||
)
|
||||
}
|
||||
|
||||
public static createAufAccount(keyPair: KeyPair, createdAt: Date): Account {
|
||||
return AccountFactory.createAccount(
|
||||
createdAt,
|
||||
hardenDerivationIndex(AUF_ACCOUNT_DERIVATION_INDEX),
|
||||
AddressType.COMMUNITY_AUF,
|
||||
keyPair,
|
||||
)
|
||||
}
|
||||
}
|
||||
34
dlt-connector/src/data/Account.repository.ts
Normal file
34
dlt-connector/src/data/Account.repository.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import { Account } from '@entity/Account'
|
||||
import { User } from '@entity/User'
|
||||
import { In } from 'typeorm'
|
||||
|
||||
import { UserIdentifier } from '@/graphql/input/UserIdentifier'
|
||||
import { getDataSource } from '@/typeorm/DataSource'
|
||||
|
||||
export const AccountRepository = getDataSource()
|
||||
.getRepository(Account)
|
||||
.extend({
|
||||
findAccountsByPublicKeys(publicKeys: Buffer[]): Promise<Account[]> {
|
||||
return this.findBy({ derive2Pubkey: In(publicKeys) })
|
||||
},
|
||||
|
||||
async findAccountByPublicKey(publicKey: Buffer | undefined): Promise<Account | undefined> {
|
||||
if (!publicKey) return undefined
|
||||
return (await this.findOneBy({ derive2Pubkey: Buffer.from(publicKey) })) ?? undefined
|
||||
},
|
||||
|
||||
async findAccountByUserIdentifier({
|
||||
uuid,
|
||||
accountNr,
|
||||
}: UserIdentifier): Promise<Account | undefined> {
|
||||
const user = await User.findOne({
|
||||
where: { gradidoID: uuid, accounts: { derivationIndex: accountNr ?? 1 } },
|
||||
relations: { accounts: true },
|
||||
})
|
||||
if (user && user.accounts?.length === 1) {
|
||||
const account = user.accounts[0]
|
||||
account.user = user
|
||||
return account
|
||||
}
|
||||
},
|
||||
})
|
||||
197
dlt-connector/src/data/Account.test.ts
Normal file
197
dlt-connector/src/data/Account.test.ts
Normal file
@ -0,0 +1,197 @@
|
||||
import 'reflect-metadata'
|
||||
import { Decimal } from 'decimal.js-light'
|
||||
|
||||
import { TestDB } from '@test/TestDB'
|
||||
|
||||
import { AccountType } from '@/graphql/enum/AccountType'
|
||||
import { UserAccountDraft } from '@/graphql/input/UserAccountDraft'
|
||||
import { UserIdentifier } from '@/graphql/input/UserIdentifier'
|
||||
|
||||
import { AccountFactory } from './Account.factory'
|
||||
import { AccountRepository } from './Account.repository'
|
||||
import { KeyPair } from './KeyPair'
|
||||
import { Mnemonic } from './Mnemonic'
|
||||
import { AddressType } from './proto/3_3/enum/AddressType'
|
||||
import { UserFactory } from './User.factory'
|
||||
import { UserLogic } from './User.logic'
|
||||
|
||||
const con = TestDB.instance
|
||||
|
||||
jest.mock('@typeorm/DataSource', () => ({
|
||||
getDataSource: jest.fn(() => TestDB.instance.dbConnect),
|
||||
}))
|
||||
|
||||
describe('data/Account test factory and repository', () => {
|
||||
const now = new Date()
|
||||
const keyPair1 = new KeyPair(new Mnemonic('62ef251edc2416f162cd24ab1711982b'))
|
||||
const keyPair2 = new KeyPair(new Mnemonic('000a0000000002000000000003000070'))
|
||||
const keyPair3 = new KeyPair(new Mnemonic('00ba541a1000020000000000300bda70'))
|
||||
const userGradidoID = '6be949ab-8198-4acf-ba63-740089081d61'
|
||||
|
||||
describe('test factory methods', () => {
|
||||
beforeAll(async () => {
|
||||
await con.setupTestDB()
|
||||
})
|
||||
afterAll(async () => {
|
||||
await con.teardownTestDB()
|
||||
})
|
||||
|
||||
it('test createAccount', () => {
|
||||
const account = AccountFactory.createAccount(now, 1, AddressType.COMMUNITY_HUMAN, keyPair1)
|
||||
expect(account).toMatchObject({
|
||||
derivationIndex: 1,
|
||||
derive2Pubkey: Buffer.from(
|
||||
'cb88043ef4833afc01d6ed9b34e1aa48e79dce5ff97c07090c6600ec05f6d994',
|
||||
'hex',
|
||||
),
|
||||
type: AddressType.COMMUNITY_HUMAN,
|
||||
createdAt: now,
|
||||
balanceCreatedAt: now,
|
||||
balanceOnConfirmation: new Decimal(0),
|
||||
balanceOnCreation: new Decimal(0),
|
||||
})
|
||||
})
|
||||
|
||||
it('test createAccountFromUserAccountDraft', () => {
|
||||
const userAccountDraft = new UserAccountDraft()
|
||||
userAccountDraft.createdAt = now.toISOString()
|
||||
userAccountDraft.accountType = AccountType.COMMUNITY_HUMAN
|
||||
userAccountDraft.user = new UserIdentifier()
|
||||
userAccountDraft.user.accountNr = 1
|
||||
const account = AccountFactory.createAccountFromUserAccountDraft(userAccountDraft, keyPair1)
|
||||
expect(account).toMatchObject({
|
||||
derivationIndex: 1,
|
||||
derive2Pubkey: Buffer.from(
|
||||
'cb88043ef4833afc01d6ed9b34e1aa48e79dce5ff97c07090c6600ec05f6d994',
|
||||
'hex',
|
||||
),
|
||||
type: AddressType.COMMUNITY_HUMAN,
|
||||
createdAt: now,
|
||||
balanceCreatedAt: now,
|
||||
balanceOnConfirmation: new Decimal(0),
|
||||
balanceOnCreation: new Decimal(0),
|
||||
})
|
||||
})
|
||||
|
||||
it('test createGmwAccount', () => {
|
||||
const account = AccountFactory.createGmwAccount(keyPair1, now)
|
||||
expect(account).toMatchObject({
|
||||
derivationIndex: 2147483649,
|
||||
derive2Pubkey: Buffer.from(
|
||||
'05f0060357bb73bd290283870fc47a10b3764f02ca26938479ed853f46145366',
|
||||
'hex',
|
||||
),
|
||||
type: AddressType.COMMUNITY_GMW,
|
||||
createdAt: now,
|
||||
balanceCreatedAt: now,
|
||||
balanceOnConfirmation: new Decimal(0),
|
||||
balanceOnCreation: new Decimal(0),
|
||||
})
|
||||
})
|
||||
|
||||
it('test createAufAccount', () => {
|
||||
const account = AccountFactory.createAufAccount(keyPair1, now)
|
||||
expect(account).toMatchObject({
|
||||
derivationIndex: 2147483650,
|
||||
derive2Pubkey: Buffer.from(
|
||||
'6c749f8693a4a58c948e5ae54df11e2db33d2f98673b56e0cf19c0132614ab59',
|
||||
'hex',
|
||||
),
|
||||
type: AddressType.COMMUNITY_AUF,
|
||||
createdAt: now,
|
||||
balanceCreatedAt: now,
|
||||
balanceOnConfirmation: new Decimal(0),
|
||||
balanceOnCreation: new Decimal(0),
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('test repository functions', () => {
|
||||
beforeAll(async () => {
|
||||
await con.setupTestDB()
|
||||
await Promise.all([
|
||||
AccountFactory.createAufAccount(keyPair1, now).save(),
|
||||
AccountFactory.createGmwAccount(keyPair1, now).save(),
|
||||
AccountFactory.createAufAccount(keyPair2, now).save(),
|
||||
AccountFactory.createGmwAccount(keyPair2, now).save(),
|
||||
AccountFactory.createAufAccount(keyPair3, now).save(),
|
||||
AccountFactory.createGmwAccount(keyPair3, now).save(),
|
||||
])
|
||||
const userAccountDraft = new UserAccountDraft()
|
||||
userAccountDraft.accountType = AccountType.COMMUNITY_HUMAN
|
||||
userAccountDraft.createdAt = now.toString()
|
||||
userAccountDraft.user = new UserIdentifier()
|
||||
userAccountDraft.user.accountNr = 1
|
||||
userAccountDraft.user.uuid = userGradidoID
|
||||
const user = UserFactory.create(userAccountDraft, keyPair1)
|
||||
const userLogic = new UserLogic(user)
|
||||
const account = AccountFactory.createAccountFromUserAccountDraft(
|
||||
userAccountDraft,
|
||||
userLogic.calculateKeyPair(keyPair1),
|
||||
)
|
||||
account.user = user
|
||||
// user is set to cascade: ['insert'] will be saved together with account
|
||||
await account.save()
|
||||
})
|
||||
afterAll(async () => {
|
||||
await con.teardownTestDB()
|
||||
})
|
||||
it('test findAccountsByPublicKeys', async () => {
|
||||
const accounts = await AccountRepository.findAccountsByPublicKeys([
|
||||
Buffer.from('6c749f8693a4a58c948e5ae54df11e2db33d2f98673b56e0cf19c0132614ab59', 'hex'),
|
||||
Buffer.from('0fa996b73b624592fe326b8500cb1e3f10026112b374d84c87d097f4d489c019', 'hex'),
|
||||
Buffer.from('0ffa996b73b624592f26b850b0cb1e3f1026112b374d84c87d017f4d489c0197', 'hex'), // invalid
|
||||
])
|
||||
expect(accounts).toHaveLength(2)
|
||||
expect(accounts).toMatchObject(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
derivationIndex: 2147483649,
|
||||
derive2Pubkey: Buffer.from(
|
||||
'0fa996b73b624592fe326b8500cb1e3f10026112b374d84c87d097f4d489c019',
|
||||
'hex',
|
||||
),
|
||||
type: AddressType.COMMUNITY_GMW,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
derivationIndex: 2147483650,
|
||||
derive2Pubkey: Buffer.from(
|
||||
'6c749f8693a4a58c948e5ae54df11e2db33d2f98673b56e0cf19c0132614ab59',
|
||||
'hex',
|
||||
),
|
||||
type: AddressType.COMMUNITY_AUF,
|
||||
}),
|
||||
]),
|
||||
)
|
||||
})
|
||||
|
||||
it('test findAccountByPublicKey', async () => {
|
||||
expect(
|
||||
await AccountRepository.findAccountByPublicKey(
|
||||
Buffer.from('6c749f8693a4a58c948e5ae54df11e2db33d2f98673b56e0cf19c0132614ab59', 'hex'),
|
||||
),
|
||||
).toMatchObject({
|
||||
derivationIndex: 2147483650,
|
||||
derive2Pubkey: Buffer.from(
|
||||
'6c749f8693a4a58c948e5ae54df11e2db33d2f98673b56e0cf19c0132614ab59',
|
||||
'hex',
|
||||
),
|
||||
type: AddressType.COMMUNITY_AUF,
|
||||
})
|
||||
})
|
||||
|
||||
it('test findAccountByUserIdentifier', async () => {
|
||||
const userIdentifier = new UserIdentifier()
|
||||
userIdentifier.accountNr = 1
|
||||
userIdentifier.uuid = userGradidoID
|
||||
expect(await AccountRepository.findAccountByUserIdentifier(userIdentifier)).toMatchObject({
|
||||
derivationIndex: 1,
|
||||
derive2Pubkey: Buffer.from(
|
||||
'2099c004a26e5387c9fbbc9bb0f552a9642d3fd7c710ae5802b775d24ff36f93',
|
||||
'hex',
|
||||
),
|
||||
type: AddressType.COMMUNITY_HUMAN,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
13
dlt-connector/src/data/BackendTransaction.factory.ts
Normal file
13
dlt-connector/src/data/BackendTransaction.factory.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { BackendTransaction } from '@entity/BackendTransaction'
|
||||
|
||||
import { TransactionDraft } from '@/graphql/input/TransactionDraft'
|
||||
|
||||
export class BackendTransactionFactory {
|
||||
public static createFromTransactionDraft(transactionDraft: TransactionDraft): BackendTransaction {
|
||||
const backendTransaction = BackendTransaction.create()
|
||||
backendTransaction.backendTransactionId = transactionDraft.backendTransactionId
|
||||
backendTransaction.typeId = transactionDraft.type
|
||||
backendTransaction.createdAt = new Date(transactionDraft.createdAt)
|
||||
return backendTransaction
|
||||
}
|
||||
}
|
||||
7
dlt-connector/src/data/BackendTransaction.repository.ts
Normal file
7
dlt-connector/src/data/BackendTransaction.repository.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { BackendTransaction } from '@entity/BackendTransaction'
|
||||
|
||||
import { getDataSource } from '@/typeorm/DataSource'
|
||||
|
||||
export const BackendTransactionRepository = getDataSource()
|
||||
.getRepository(BackendTransaction)
|
||||
.extend({})
|
||||
76
dlt-connector/src/data/Community.repository.ts
Normal file
76
dlt-connector/src/data/Community.repository.ts
Normal file
@ -0,0 +1,76 @@
|
||||
import { Community } from '@entity/Community'
|
||||
import { FindOptionsSelect, In, IsNull, Not } from 'typeorm'
|
||||
|
||||
import { CommunityArg } from '@/graphql/arg/CommunityArg'
|
||||
import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType'
|
||||
import { CommunityDraft } from '@/graphql/input/CommunityDraft'
|
||||
import { UserIdentifier } from '@/graphql/input/UserIdentifier'
|
||||
import { TransactionError } from '@/graphql/model/TransactionError'
|
||||
import { LogError } from '@/server/LogError'
|
||||
import { getDataSource } from '@/typeorm/DataSource'
|
||||
import { iotaTopicFromCommunityUUID } from '@/utils/typeConverter'
|
||||
|
||||
import { KeyPair } from './KeyPair'
|
||||
|
||||
export const CommunityRepository = getDataSource()
|
||||
.getRepository(Community)
|
||||
.extend({
|
||||
async isExist(community: CommunityDraft | string): Promise<boolean> {
|
||||
const iotaTopic =
|
||||
community instanceof CommunityDraft ? iotaTopicFromCommunityUUID(community.uuid) : community
|
||||
const result = await this.find({
|
||||
where: { iotaTopic },
|
||||
})
|
||||
return result.length > 0
|
||||
},
|
||||
|
||||
async findByCommunityArg({ uuid, foreign, confirmed }: CommunityArg): Promise<Community[]> {
|
||||
return await this.find({
|
||||
where: {
|
||||
...(uuid && { iotaTopic: iotaTopicFromCommunityUUID(uuid) }),
|
||||
...(foreign && { foreign }),
|
||||
...(confirmed && { confirmedAt: Not(IsNull()) }),
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
async findByCommunityUuid(communityUuid: string): Promise<Community | null> {
|
||||
return await this.findOneBy({ iotaTopic: iotaTopicFromCommunityUUID(communityUuid) })
|
||||
},
|
||||
|
||||
async findByIotaTopic(iotaTopic: string): Promise<Community | null> {
|
||||
return await this.findOneBy({ iotaTopic })
|
||||
},
|
||||
|
||||
findCommunitiesByTopics(topics: string[]): Promise<Community[]> {
|
||||
return this.findBy({ iotaTopic: In(topics) })
|
||||
},
|
||||
|
||||
async getCommunityForUserIdentifier(
|
||||
identifier: UserIdentifier,
|
||||
): Promise<Community | undefined> {
|
||||
if (!identifier.communityUuid) {
|
||||
throw new TransactionError(TransactionErrorType.MISSING_PARAMETER, 'community uuid not set')
|
||||
}
|
||||
return (
|
||||
(await this.findOneBy({
|
||||
iotaTopic: iotaTopicFromCommunityUUID(identifier.communityUuid),
|
||||
})) ?? undefined
|
||||
)
|
||||
},
|
||||
|
||||
findAll(select: FindOptionsSelect<Community>): Promise<Community[]> {
|
||||
return this.find({ select })
|
||||
},
|
||||
|
||||
async loadHomeCommunityKeyPair(): Promise<KeyPair> {
|
||||
const community = await this.findOneOrFail({
|
||||
where: { foreign: false },
|
||||
select: { rootChaincode: true, rootPubkey: true, rootPrivkey: true },
|
||||
})
|
||||
if (!community.rootChaincode || !community.rootPrivkey) {
|
||||
throw new LogError('Missing chaincode or private key for home community')
|
||||
}
|
||||
return new KeyPair(community)
|
||||
},
|
||||
})
|
||||
87
dlt-connector/src/data/KeyPair.ts
Normal file
87
dlt-connector/src/data/KeyPair.ts
Normal file
@ -0,0 +1,87 @@
|
||||
import { Community } from '@entity/Community'
|
||||
|
||||
// https://www.npmjs.com/package/bip32-ed25519
|
||||
import { LogError } from '@/server/LogError'
|
||||
|
||||
import { toPublic, derivePrivate, sign, verify, generateFromSeed } from 'bip32-ed25519'
|
||||
|
||||
import { Mnemonic } from './Mnemonic'
|
||||
|
||||
/**
|
||||
* Class Managing Key Pair and also generate, sign and verify signature with it
|
||||
*/
|
||||
export class KeyPair {
|
||||
private _publicKey: Buffer
|
||||
private _chainCode: Buffer
|
||||
private _privateKey: Buffer
|
||||
|
||||
/**
|
||||
* @param input: Mnemonic = Mnemonic or Passphrase which work as seed for generating algorithms
|
||||
* @param input: Buffer = extended private key, returned from bip32-ed25519 generateFromSeed or from derivePrivate
|
||||
* @param input: Community = community entity with keys loaded from db
|
||||
*
|
||||
*/
|
||||
public constructor(input: Mnemonic | Buffer | Community) {
|
||||
if (input instanceof Mnemonic) {
|
||||
this.loadFromExtendedPrivateKey(generateFromSeed(input.seed))
|
||||
} else if (input instanceof Buffer) {
|
||||
this.loadFromExtendedPrivateKey(input)
|
||||
} else if (input instanceof Community) {
|
||||
if (!input.rootPrivkey || !input.rootChaincode || !input.rootPubkey) {
|
||||
throw new LogError('missing private key or chaincode or public key in commmunity entity')
|
||||
}
|
||||
this._privateKey = input.rootPrivkey
|
||||
this._publicKey = input.rootPubkey
|
||||
this._chainCode = input.rootChaincode
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* copy keys to community entity
|
||||
* @param community
|
||||
*/
|
||||
public fillInCommunityKeys(community: Community) {
|
||||
community.rootPubkey = this._publicKey
|
||||
community.rootPrivkey = this._privateKey
|
||||
community.rootChaincode = this._chainCode
|
||||
}
|
||||
|
||||
private loadFromExtendedPrivateKey(extendedPrivateKey: Buffer) {
|
||||
if (extendedPrivateKey.length !== 96) {
|
||||
throw new LogError('invalid extended private key')
|
||||
}
|
||||
this._privateKey = extendedPrivateKey.subarray(0, 64)
|
||||
this._chainCode = extendedPrivateKey.subarray(64, 96)
|
||||
this._publicKey = toPublic(extendedPrivateKey).subarray(0, 32)
|
||||
}
|
||||
|
||||
public getExtendPrivateKey(): Buffer {
|
||||
return Buffer.concat([this._privateKey, this._chainCode])
|
||||
}
|
||||
|
||||
public getExtendPublicKey(): Buffer {
|
||||
return Buffer.concat([this._publicKey, this._chainCode])
|
||||
}
|
||||
|
||||
public get publicKey(): Buffer {
|
||||
return this._publicKey
|
||||
}
|
||||
|
||||
public derive(path: number[]): KeyPair {
|
||||
const extendedPrivateKey = this.getExtendPrivateKey()
|
||||
return new KeyPair(
|
||||
path.reduce(
|
||||
(extendPrivateKey: Buffer, node: number) => derivePrivate(extendPrivateKey, node),
|
||||
extendedPrivateKey,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
public sign(message: Buffer): Buffer {
|
||||
return sign(message, this.getExtendPrivateKey())
|
||||
}
|
||||
|
||||
public verify(message: Buffer, signature: Buffer): boolean {
|
||||
return verify(message, signature, this.getExtendPublicKey())
|
||||
}
|
||||
}
|
||||
25
dlt-connector/src/data/Mnemonic.ts
Normal file
25
dlt-connector/src/data/Mnemonic.ts
Normal file
@ -0,0 +1,25 @@
|
||||
// https://www.npmjs.com/package/bip39
|
||||
import { entropyToMnemonic, mnemonicToSeedSync } from 'bip39'
|
||||
// eslint-disable-next-line camelcase
|
||||
import { randombytes_buf } from 'sodium-native'
|
||||
|
||||
export class Mnemonic {
|
||||
private _passphrase = ''
|
||||
public constructor(seed?: Buffer | string) {
|
||||
if (seed) {
|
||||
this._passphrase = entropyToMnemonic(seed)
|
||||
return
|
||||
}
|
||||
const entropy = Buffer.alloc(256)
|
||||
randombytes_buf(entropy)
|
||||
this._passphrase = entropyToMnemonic(entropy)
|
||||
}
|
||||
|
||||
public get passphrase(): string {
|
||||
return this._passphrase
|
||||
}
|
||||
|
||||
public get seed(): Buffer {
|
||||
return mnemonicToSeedSync(this._passphrase)
|
||||
}
|
||||
}
|
||||
179
dlt-connector/src/data/Transaction.builder.ts
Normal file
179
dlt-connector/src/data/Transaction.builder.ts
Normal file
@ -0,0 +1,179 @@
|
||||
import { Account } from '@entity/Account'
|
||||
import { Community } from '@entity/Community'
|
||||
import { Transaction } from '@entity/Transaction'
|
||||
|
||||
import { GradidoTransaction } from '@/data/proto/3_3/GradidoTransaction'
|
||||
import { TransactionBody } from '@/data/proto/3_3/TransactionBody'
|
||||
import { TransactionDraft } from '@/graphql/input/TransactionDraft'
|
||||
import { UserIdentifier } from '@/graphql/input/UserIdentifier'
|
||||
import { LogError } from '@/server/LogError'
|
||||
import { bodyBytesToTransactionBody, transactionBodyToBodyBytes } from '@/utils/typeConverter'
|
||||
|
||||
import { AccountRepository } from './Account.repository'
|
||||
import { BackendTransactionFactory } from './BackendTransaction.factory'
|
||||
import { CommunityRepository } from './Community.repository'
|
||||
import { TransactionBodyBuilder } from './proto/TransactionBody.builder'
|
||||
|
||||
export class TransactionBuilder {
|
||||
private transaction: Transaction
|
||||
|
||||
// https://refactoring.guru/design-patterns/builder/typescript/example
|
||||
/**
|
||||
* A fresh builder instance should contain a blank product object, which is
|
||||
* used in further assembly.
|
||||
*/
|
||||
constructor() {
|
||||
this.reset()
|
||||
}
|
||||
|
||||
public reset(): void {
|
||||
this.transaction = Transaction.create()
|
||||
}
|
||||
|
||||
/**
|
||||
* Concrete Builders are supposed to provide their own methods for
|
||||
* retrieving results. That's because various types of builders may create
|
||||
* entirely different products that don't follow the same interface.
|
||||
* Therefore, such methods cannot be declared in the base Builder interface
|
||||
* (at least in a statically typed programming language).
|
||||
*
|
||||
* Usually, after returning the end result to the client, a builder instance
|
||||
* is expected to be ready to start producing another product. That's why
|
||||
* it's a usual practice to call the reset method at the end of the
|
||||
* `getProduct` method body. However, this behavior is not mandatory, and
|
||||
* you can make your builders wait for an explicit reset call from the
|
||||
* client code before disposing of the previous result.
|
||||
*/
|
||||
public build(): Transaction {
|
||||
const result = this.transaction
|
||||
this.reset()
|
||||
return result
|
||||
}
|
||||
|
||||
// return transaction without calling reset
|
||||
public getTransaction(): Transaction {
|
||||
return this.transaction
|
||||
}
|
||||
|
||||
public getCommunity(): Community {
|
||||
return this.transaction.community
|
||||
}
|
||||
|
||||
public setSigningAccount(signingAccount: Account): TransactionBuilder {
|
||||
this.transaction.signingAccount = signingAccount
|
||||
return this
|
||||
}
|
||||
|
||||
public setRecipientAccount(recipientAccount: Account): TransactionBuilder {
|
||||
this.transaction.recipientAccount = recipientAccount
|
||||
return this
|
||||
}
|
||||
|
||||
public setCommunity(community: Community): TransactionBuilder {
|
||||
this.transaction.community = community
|
||||
return this
|
||||
}
|
||||
|
||||
public setOtherCommunity(otherCommunity?: Community): TransactionBuilder {
|
||||
if (!this.transaction.community) {
|
||||
throw new LogError('Please set community first!')
|
||||
}
|
||||
|
||||
this.transaction.otherCommunity =
|
||||
otherCommunity &&
|
||||
this.transaction.community &&
|
||||
this.transaction.community.id !== otherCommunity.id
|
||||
? otherCommunity
|
||||
: undefined
|
||||
return this
|
||||
}
|
||||
|
||||
public setSignature(signature: Buffer): TransactionBuilder {
|
||||
this.transaction.signature = signature
|
||||
return this
|
||||
}
|
||||
|
||||
public addBackendTransaction(transactionDraft: TransactionDraft): TransactionBuilder {
|
||||
if (!this.transaction.backendTransactions) {
|
||||
this.transaction.backendTransactions = []
|
||||
}
|
||||
this.transaction.backendTransactions.push(
|
||||
BackendTransactionFactory.createFromTransactionDraft(transactionDraft),
|
||||
)
|
||||
return this
|
||||
}
|
||||
|
||||
public async setSenderCommunityFromSenderUser(
|
||||
senderUser: UserIdentifier,
|
||||
): Promise<TransactionBuilder> {
|
||||
// get sender community
|
||||
const community = await CommunityRepository.getCommunityForUserIdentifier(senderUser)
|
||||
if (!community) {
|
||||
throw new LogError("couldn't find community for transaction")
|
||||
}
|
||||
return this.setCommunity(community)
|
||||
}
|
||||
|
||||
public async setOtherCommunityFromRecipientUser(
|
||||
recipientUser: UserIdentifier,
|
||||
): Promise<TransactionBuilder> {
|
||||
// get recipient community
|
||||
const otherCommunity = await CommunityRepository.getCommunityForUserIdentifier(recipientUser)
|
||||
return this.setOtherCommunity(otherCommunity)
|
||||
}
|
||||
|
||||
public async fromGradidoTransactionSearchForAccounts(
|
||||
gradidoTransaction: GradidoTransaction,
|
||||
): Promise<TransactionBuilder> {
|
||||
this.transaction.bodyBytes = Buffer.from(gradidoTransaction.bodyBytes)
|
||||
const transactionBody = bodyBytesToTransactionBody(this.transaction.bodyBytes)
|
||||
this.fromTransactionBody(transactionBody)
|
||||
|
||||
const firstSigPair = gradidoTransaction.getFirstSignature()
|
||||
// TODO: adapt if transactions with more than one signatures where added
|
||||
|
||||
// get recipient and signer accounts if not already set
|
||||
this.transaction.signingAccount ??= await AccountRepository.findAccountByPublicKey(
|
||||
firstSigPair.pubKey,
|
||||
)
|
||||
this.transaction.recipientAccount ??= await AccountRepository.findAccountByPublicKey(
|
||||
transactionBody.getRecipientPublicKey(),
|
||||
)
|
||||
this.transaction.signature = Buffer.from(firstSigPair.signature)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public fromGradidoTransaction(gradidoTransaction: GradidoTransaction): TransactionBuilder {
|
||||
this.transaction.bodyBytes = Buffer.from(gradidoTransaction.bodyBytes)
|
||||
const transactionBody = bodyBytesToTransactionBody(this.transaction.bodyBytes)
|
||||
this.fromTransactionBody(transactionBody)
|
||||
|
||||
const firstSigPair = gradidoTransaction.getFirstSignature()
|
||||
// TODO: adapt if transactions with more than one signatures where added
|
||||
this.transaction.signature = Buffer.from(firstSigPair.signature)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public fromTransactionBody(transactionBody: TransactionBody): TransactionBuilder {
|
||||
transactionBody.fillTransactionRecipe(this.transaction)
|
||||
this.transaction.bodyBytes ??= transactionBodyToBodyBytes(transactionBody)
|
||||
return this
|
||||
}
|
||||
|
||||
public fromTransactionBodyBuilder(
|
||||
transactionBodyBuilder: TransactionBodyBuilder,
|
||||
): TransactionBuilder {
|
||||
const signingAccount = transactionBodyBuilder.getSigningAccount()
|
||||
if (signingAccount) {
|
||||
this.setSigningAccount(signingAccount)
|
||||
}
|
||||
const recipientAccount = transactionBodyBuilder.getRecipientAccount()
|
||||
if (recipientAccount) {
|
||||
this.setRecipientAccount(recipientAccount)
|
||||
}
|
||||
this.fromTransactionBody(transactionBodyBuilder.getTransactionBody())
|
||||
return this
|
||||
}
|
||||
}
|
||||
43
dlt-connector/src/data/Transaction.repository.ts
Normal file
43
dlt-connector/src/data/Transaction.repository.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { Transaction } from '@entity/Transaction'
|
||||
import { IsNull } from 'typeorm'
|
||||
|
||||
import { getDataSource } from '@/typeorm/DataSource'
|
||||
|
||||
// https://www.artima.com/articles/the-dci-architecture-a-new-vision-of-object-oriented-programming
|
||||
export const TransactionRepository = getDataSource()
|
||||
.getRepository(Transaction)
|
||||
.extend({
|
||||
findBySignature(signature: Buffer): Promise<Transaction | null> {
|
||||
return this.findOneBy({ signature: Buffer.from(signature) })
|
||||
},
|
||||
findByMessageId(iotaMessageId: string): Promise<Transaction | null> {
|
||||
return this.findOneBy({ iotaMessageId: Buffer.from(iotaMessageId, 'hex') })
|
||||
},
|
||||
async getNextPendingTransaction(): Promise<Transaction | null> {
|
||||
return await this.findOne({
|
||||
where: { iotaMessageId: IsNull() },
|
||||
order: { createdAt: 'ASC' },
|
||||
relations: { signingAccount: true },
|
||||
})
|
||||
},
|
||||
findExistingTransactionAndMissingMessageIds(messageIDsHex: string[]): Promise<Transaction[]> {
|
||||
return this.createQueryBuilder('Transaction')
|
||||
.where('HEX(Transaction.iota_message_id) IN (:...messageIDs)', {
|
||||
messageIDs: messageIDsHex,
|
||||
})
|
||||
.leftJoinAndSelect('Transaction.community', 'Community')
|
||||
.leftJoinAndSelect('Transaction.otherCommunity', 'OtherCommunity')
|
||||
.leftJoinAndSelect('Transaction.recipientAccount', 'RecipientAccount')
|
||||
.leftJoinAndSelect('Transaction.backendTransactions', 'BackendTransactions')
|
||||
.leftJoinAndSelect('RecipientAccount.user', 'RecipientUser')
|
||||
.leftJoinAndSelect('Transaction.signingAccount', 'SigningAccount')
|
||||
.leftJoinAndSelect('SigningAccount.user', 'SigningUser')
|
||||
.getMany()
|
||||
},
|
||||
removeConfirmedTransaction(transactions: Transaction[]): Transaction[] {
|
||||
return transactions.filter(
|
||||
(transaction: Transaction) =>
|
||||
transaction.runningHash === undefined || transaction.runningHash.length === 0,
|
||||
)
|
||||
},
|
||||
})
|
||||
18
dlt-connector/src/data/User.factory.ts
Normal file
18
dlt-connector/src/data/User.factory.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { User } from '@entity/User'
|
||||
|
||||
import { UserAccountDraft } from '@/graphql/input/UserAccountDraft'
|
||||
|
||||
import { KeyPair } from './KeyPair'
|
||||
import { UserLogic } from './User.logic'
|
||||
|
||||
export class UserFactory {
|
||||
static create(userAccountDraft: UserAccountDraft, parentKeys: KeyPair): User {
|
||||
const user = User.create()
|
||||
user.createdAt = new Date(userAccountDraft.createdAt)
|
||||
user.gradidoID = userAccountDraft.user.uuid
|
||||
const userLogic = new UserLogic(user)
|
||||
// store generated pubkey into entity
|
||||
userLogic.calculateKeyPair(parentKeys)
|
||||
return user
|
||||
}
|
||||
}
|
||||
42
dlt-connector/src/data/User.logic.ts
Normal file
42
dlt-connector/src/data/User.logic.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { User } from '@entity/User'
|
||||
|
||||
import { LogError } from '@/server/LogError'
|
||||
import { hardenDerivationIndex } from '@/utils/derivationHelper'
|
||||
import { uuid4ToBuffer } from '@/utils/typeConverter'
|
||||
|
||||
import { KeyPair } from './KeyPair'
|
||||
|
||||
export class UserLogic {
|
||||
// eslint-disable-next-line no-useless-constructor
|
||||
constructor(private user: User) {}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param parentKeys if undefined use home community key pair
|
||||
* @returns
|
||||
*/
|
||||
|
||||
calculateKeyPair = (parentKeys: KeyPair): KeyPair => {
|
||||
if (!this.user.gradidoID) {
|
||||
throw new LogError('missing GradidoID for user.', { id: this.user.id })
|
||||
}
|
||||
// example gradido id: 03857ac1-9cc2-483e-8a91-e5b10f5b8d16 =>
|
||||
// wholeHex: '03857ac19cc2483e8a91e5b10f5b8d16']
|
||||
const wholeHex = uuid4ToBuffer(this.user.gradidoID)
|
||||
const parts = []
|
||||
for (let i = 0; i < 4; i++) {
|
||||
parts[i] = hardenDerivationIndex(wholeHex.subarray(i * 4, (i + 1) * 4).readUInt32BE())
|
||||
}
|
||||
// parts: [2206563009, 2629978174, 2324817329, 2405141782]
|
||||
const keyPair = parentKeys.derive(parts)
|
||||
if (this.user.derive1Pubkey && this.user.derive1Pubkey.compare(keyPair.publicKey) !== 0) {
|
||||
throw new LogError(
|
||||
'The freshly derived public key does not correspond to the stored public key',
|
||||
)
|
||||
}
|
||||
if (!this.user.derive1Pubkey) {
|
||||
this.user.derive1Pubkey = keyPair.publicKey
|
||||
}
|
||||
return keyPair
|
||||
}
|
||||
}
|
||||
24
dlt-connector/src/data/User.repository.ts
Normal file
24
dlt-connector/src/data/User.repository.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { Account } from '@entity/Account'
|
||||
import { User } from '@entity/User'
|
||||
|
||||
import { UserIdentifier } from '@/graphql/input/UserIdentifier'
|
||||
import { getDataSource } from '@/typeorm/DataSource'
|
||||
|
||||
export const UserRepository = getDataSource()
|
||||
.getRepository(User)
|
||||
.extend({
|
||||
async findAccountByUserIdentifier({
|
||||
uuid,
|
||||
accountNr,
|
||||
}: UserIdentifier): Promise<Account | undefined> {
|
||||
const user = await this.findOne({
|
||||
where: { gradidoID: uuid, accounts: { derivationIndex: accountNr ?? 1 } },
|
||||
relations: { accounts: true },
|
||||
})
|
||||
if (user && user.accounts?.length === 1) {
|
||||
const account = user.accounts[0]
|
||||
account.user = user
|
||||
return account
|
||||
}
|
||||
},
|
||||
})
|
||||
35
dlt-connector/src/data/proto/3_3/CommunityRoot.ts
Normal file
35
dlt-connector/src/data/proto/3_3/CommunityRoot.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { Community } from '@entity/Community'
|
||||
import { Transaction } from '@entity/Transaction'
|
||||
import { Field, Message } from 'protobufjs'
|
||||
|
||||
import { AbstractTransaction } from '../AbstractTransaction'
|
||||
|
||||
// https://www.npmjs.com/package/@apollo/protobufjs
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
export class CommunityRoot extends Message<CommunityRoot> implements AbstractTransaction {
|
||||
public constructor(community?: Community) {
|
||||
if (community) {
|
||||
super({
|
||||
rootPubkey: community.rootPubkey,
|
||||
gmwPubkey: community.gmwAccount?.derive2Pubkey,
|
||||
aufPubkey: community.aufAccount?.derive2Pubkey,
|
||||
})
|
||||
} else {
|
||||
super()
|
||||
}
|
||||
}
|
||||
|
||||
@Field.d(1, 'bytes')
|
||||
public rootPubkey: Buffer
|
||||
|
||||
// community public budget account
|
||||
@Field.d(2, 'bytes')
|
||||
public gmwPubkey: Buffer
|
||||
|
||||
// community compensation and environment founds account
|
||||
@Field.d(3, 'bytes')
|
||||
public aufPubkey: Buffer
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function, @typescript-eslint/no-unused-vars
|
||||
public fillTransactionRecipe(recipe: Transaction): void {}
|
||||
}
|
||||
@ -1,4 +1,7 @@
|
||||
import { Field, Message } from '@apollo/protobufjs'
|
||||
import { Field, Message } from 'protobufjs'
|
||||
|
||||
import { base64ToBuffer } from '@/utils/typeConverter'
|
||||
|
||||
import { GradidoTransaction } from './GradidoTransaction'
|
||||
import { TimestampSeconds } from './TimestampSeconds'
|
||||
|
||||
@ -10,9 +13,13 @@ import { TimestampSeconds } from './TimestampSeconds'
|
||||
|
||||
// https://www.npmjs.com/package/@apollo/protobufjs
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
export class GradidoConfirmedTransaction extends Message<GradidoConfirmedTransaction> {
|
||||
export class ConfirmedTransaction extends Message<ConfirmedTransaction> {
|
||||
static fromBase64(base64: string): ConfirmedTransaction {
|
||||
return ConfirmedTransaction.decode(new Uint8Array(base64ToBuffer(base64)))
|
||||
}
|
||||
|
||||
@Field.d(1, 'uint64')
|
||||
id: number
|
||||
id: Long
|
||||
|
||||
@Field.d(2, 'GradidoTransaction')
|
||||
transaction: GradidoTransaction
|
||||
@ -1,9 +1,11 @@
|
||||
import 'reflect-metadata'
|
||||
import { TransactionDraft } from '@/graphql/input/TransactionDraft'
|
||||
import { GradidoCreation } from './GradidoCreation'
|
||||
import { TransactionError } from '@/graphql/model/TransactionError'
|
||||
import { TransactionErrorType } from '@enum/TransactionErrorType'
|
||||
|
||||
import { TransactionDraft } from '@/graphql/input/TransactionDraft'
|
||||
import { TransactionError } from '@/graphql/model/TransactionError'
|
||||
|
||||
import { GradidoCreation } from './GradidoCreation'
|
||||
|
||||
describe('proto/3.3/GradidoCreation', () => {
|
||||
it('test with missing targetDate', () => {
|
||||
const transactionDraft = new TransactionDraft()
|
||||
53
dlt-connector/src/data/proto/3_3/GradidoCreation.ts
Normal file
53
dlt-connector/src/data/proto/3_3/GradidoCreation.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import { Account } from '@entity/Account'
|
||||
import { Transaction } from '@entity/Transaction'
|
||||
import { Decimal } from 'decimal.js-light'
|
||||
import { Field, Message } from 'protobufjs'
|
||||
|
||||
import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType'
|
||||
import { TransactionDraft } from '@/graphql/input/TransactionDraft'
|
||||
import { TransactionError } from '@/graphql/model/TransactionError'
|
||||
|
||||
import { AbstractTransaction } from '../AbstractTransaction'
|
||||
|
||||
import { TimestampSeconds } from './TimestampSeconds'
|
||||
import { TransferAmount } from './TransferAmount'
|
||||
|
||||
// need signature from group admin or
|
||||
// percent of group users another than the receiver
|
||||
// https://www.npmjs.com/package/@apollo/protobufjs
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
export class GradidoCreation extends Message<GradidoCreation> implements AbstractTransaction {
|
||||
constructor(transaction?: TransactionDraft, recipientAccount?: Account) {
|
||||
if (transaction) {
|
||||
if (!transaction.targetDate) {
|
||||
throw new TransactionError(
|
||||
TransactionErrorType.MISSING_PARAMETER,
|
||||
'missing targetDate for contribution',
|
||||
)
|
||||
}
|
||||
super({
|
||||
recipient: new TransferAmount({
|
||||
amount: transaction.amount.toString(),
|
||||
pubkey: recipientAccount?.derive2Pubkey,
|
||||
}),
|
||||
targetDate: new TimestampSeconds(new Date(transaction.targetDate)),
|
||||
})
|
||||
} else {
|
||||
super()
|
||||
}
|
||||
}
|
||||
|
||||
// recipient: TransferAmount contain
|
||||
// - recipient public key
|
||||
// - amount
|
||||
// - communityId // only set if not the same as recipient community
|
||||
@Field.d(1, TransferAmount)
|
||||
public recipient: TransferAmount
|
||||
|
||||
@Field.d(3, 'TimestampSeconds')
|
||||
public targetDate: TimestampSeconds
|
||||
|
||||
public fillTransactionRecipe(recipe: Transaction): void {
|
||||
recipe.amount = new Decimal(this.recipient.amount ?? 0)
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,8 @@
|
||||
import { Field, Message } from '@apollo/protobufjs'
|
||||
import { Transaction } from '@entity/Transaction'
|
||||
import Decimal from 'decimal.js-light'
|
||||
import { Field, Message } from 'protobufjs'
|
||||
|
||||
import { AbstractTransaction } from '../AbstractTransaction'
|
||||
|
||||
import { GradidoTransfer } from './GradidoTransfer'
|
||||
import { TimestampSeconds } from './TimestampSeconds'
|
||||
@ -10,8 +14,11 @@ import { TimestampSeconds } from './TimestampSeconds'
|
||||
// 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> {
|
||||
export class GradidoDeferredTransfer
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
extends Message<GradidoDeferredTransfer>
|
||||
implements AbstractTransaction
|
||||
{
|
||||
// amount is amount with decay for time span between transaction was received and timeout
|
||||
// useable amount can be calculated
|
||||
// recipient address don't need to be registered in blockchain with register address
|
||||
@ -28,4 +35,8 @@ export class GradidoDeferredTransfer extends Message<GradidoDeferredTransfer> {
|
||||
|
||||
// split for n recipient
|
||||
// max gradido per recipient? or per transaction with cool down?
|
||||
|
||||
public fillTransactionRecipe(recipe: Transaction): void {
|
||||
recipe.amount = new Decimal(this.transfer.sender.amount ?? 0)
|
||||
}
|
||||
}
|
||||
44
dlt-connector/src/data/proto/3_3/GradidoTransaction.ts
Normal file
44
dlt-connector/src/data/proto/3_3/GradidoTransaction.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import { Field, Message } from 'protobufjs'
|
||||
|
||||
import { LogError } from '@/server/LogError'
|
||||
|
||||
import { SignatureMap } from './SignatureMap'
|
||||
import { SignaturePair } from './SignaturePair'
|
||||
import { TransactionBody } from './TransactionBody'
|
||||
|
||||
// https://www.npmjs.com/package/@apollo/protobufjs
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
export class GradidoTransaction extends Message<GradidoTransaction> {
|
||||
constructor(body?: TransactionBody) {
|
||||
if (body) {
|
||||
super({
|
||||
sigMap: new SignatureMap(),
|
||||
bodyBytes: Buffer.from(TransactionBody.encode(body).finish()),
|
||||
})
|
||||
} else {
|
||||
super()
|
||||
}
|
||||
}
|
||||
|
||||
@Field.d(1, SignatureMap)
|
||||
public sigMap: SignatureMap
|
||||
|
||||
// inspired by Hedera
|
||||
// bodyBytes are the payload for signature
|
||||
// bodyBytes are serialized TransactionBody
|
||||
@Field.d(2, 'bytes')
|
||||
public bodyBytes: Buffer
|
||||
|
||||
// if it is a cross group transaction the parent message
|
||||
// id from outbound transaction or other by cross
|
||||
@Field.d(3, 'bytes')
|
||||
public parentMessageId?: Buffer
|
||||
|
||||
getFirstSignature(): SignaturePair {
|
||||
const sigPair = this.sigMap.sigPair
|
||||
if (sigPair.length !== 1) {
|
||||
throw new LogError("signature count don't like expected")
|
||||
}
|
||||
return sigPair[0]
|
||||
}
|
||||
}
|
||||
49
dlt-connector/src/data/proto/3_3/GradidoTransfer.ts
Normal file
49
dlt-connector/src/data/proto/3_3/GradidoTransfer.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import { Account } from '@entity/Account'
|
||||
import { Transaction } from '@entity/Transaction'
|
||||
import Decimal from 'decimal.js-light'
|
||||
import { Field, Message } from 'protobufjs'
|
||||
|
||||
import { TransactionDraft } from '@/graphql/input/TransactionDraft'
|
||||
|
||||
import { AbstractTransaction } from '../AbstractTransaction'
|
||||
|
||||
import { TransferAmount } from './TransferAmount'
|
||||
|
||||
// https://www.npmjs.com/package/@apollo/protobufjs
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
export class GradidoTransfer extends Message<GradidoTransfer> implements AbstractTransaction {
|
||||
constructor(
|
||||
transaction?: TransactionDraft,
|
||||
signingAccount?: Account,
|
||||
recipientAccount?: Account,
|
||||
coinOrigin?: string,
|
||||
) {
|
||||
if (transaction) {
|
||||
super({
|
||||
sender: new TransferAmount({
|
||||
amount: transaction.amount.toString(),
|
||||
pubkey: signingAccount?.derive2Pubkey,
|
||||
communityId: coinOrigin,
|
||||
}),
|
||||
recipient: recipientAccount?.derive2Pubkey,
|
||||
})
|
||||
} else {
|
||||
super()
|
||||
}
|
||||
}
|
||||
|
||||
// sender: TransferAmount contain
|
||||
// - sender public key
|
||||
// - amount
|
||||
// - communityId // only set if not the same as sender and recipient community
|
||||
@Field.d(1, TransferAmount)
|
||||
public sender: TransferAmount
|
||||
|
||||
// the recipient public key
|
||||
@Field.d(2, 'bytes')
|
||||
public recipient: Buffer
|
||||
|
||||
public fillTransactionRecipe(recipe: Transaction): void {
|
||||
recipe.amount = new Decimal(this.sender?.amount ?? 0)
|
||||
}
|
||||
}
|
||||
@ -1,10 +1,14 @@
|
||||
import { Field, Message } from '@apollo/protobufjs'
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import { Transaction } from '@entity/Transaction'
|
||||
import { Field, Message } from 'protobufjs'
|
||||
|
||||
import { AbstractTransaction } from '../AbstractTransaction'
|
||||
|
||||
// connect group together
|
||||
// only CrossGroupType CROSS (in TransactionBody)
|
||||
// https://www.npmjs.com/package/@apollo/protobufjs
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
export class GroupFriendsUpdate extends Message<GroupFriendsUpdate> {
|
||||
export class GroupFriendsUpdate extends Message<GroupFriendsUpdate> implements AbstractTransaction {
|
||||
// if set to true, colors of this both groups are trait as the same
|
||||
// on creation user get coins still in there color
|
||||
// on transfer into another group which a connection exist,
|
||||
@ -12,4 +16,8 @@ export class GroupFriendsUpdate extends Message<GroupFriendsUpdate> {
|
||||
// (if fusion between src coin and dst coin is enabled)
|
||||
@Field.d(1, 'bool')
|
||||
public colorFusion: boolean
|
||||
|
||||
public fillTransactionRecipe(recipe: Transaction): void {
|
||||
throw new Error('Method not implemented.')
|
||||
}
|
||||
}
|
||||
29
dlt-connector/src/data/proto/3_3/RegisterAddress.ts
Normal file
29
dlt-connector/src/data/proto/3_3/RegisterAddress.ts
Normal file
@ -0,0 +1,29 @@
|
||||
/* eslint-disable @typescript-eslint/no-empty-function */
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import { Transaction } from '@entity/Transaction'
|
||||
import { Field, Message } from 'protobufjs'
|
||||
|
||||
import { AddressType } from '@/data/proto/3_3/enum/AddressType'
|
||||
|
||||
import { AbstractTransaction } from '../AbstractTransaction'
|
||||
|
||||
// https://www.npmjs.com/package/@apollo/protobufjs
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
export class RegisterAddress extends Message<RegisterAddress> implements AbstractTransaction {
|
||||
@Field.d(1, 'bytes')
|
||||
public userPubkey: Buffer
|
||||
|
||||
@Field.d(2, AddressType)
|
||||
public addressType: AddressType
|
||||
|
||||
@Field.d(3, 'bytes')
|
||||
public nameHash: Buffer
|
||||
|
||||
@Field.d(4, 'bytes')
|
||||
public accountPubkey: Buffer
|
||||
|
||||
@Field.d(5, 'uint32')
|
||||
public derivationIndex?: number
|
||||
|
||||
public fillTransactionRecipe(_recipe: Transaction): void {}
|
||||
}
|
||||
@ -1,10 +1,14 @@
|
||||
import { Field, Message } from '@apollo/protobufjs'
|
||||
import { Field, Message } from 'protobufjs'
|
||||
|
||||
import { SignaturePair } from './SignaturePair'
|
||||
|
||||
// https://www.npmjs.com/package/@apollo/protobufjs
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
export class SignatureMap extends Message<SignatureMap> {
|
||||
constructor() {
|
||||
super({ sigPair: [] })
|
||||
}
|
||||
|
||||
@Field.d(1, SignaturePair, 'repeated')
|
||||
public sigPair: SignaturePair
|
||||
public sigPair: SignaturePair[]
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
import { Field, Message } from '@apollo/protobufjs'
|
||||
import { Field, Message } from 'protobufjs'
|
||||
|
||||
// https://www.npmjs.com/package/@apollo/protobufjs
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
@ -8,4 +8,8 @@ export class SignaturePair extends Message<SignaturePair> {
|
||||
|
||||
@Field.d(2, 'bytes')
|
||||
public signature: Buffer
|
||||
|
||||
public validate(): boolean {
|
||||
return this.pubKey.length === 32 && this.signature.length === 64
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
import { Field, Message } from '@apollo/protobufjs'
|
||||
import { Field, Message } from 'protobufjs'
|
||||
|
||||
// https://www.npmjs.com/package/@apollo/protobufjs
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
@ -1,4 +1,4 @@
|
||||
import { Field, Message } from '@apollo/protobufjs'
|
||||
import { Field, Message } from 'protobufjs'
|
||||
|
||||
// https://www.npmjs.com/package/@apollo/protobufjs
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
133
dlt-connector/src/data/proto/3_3/TransactionBody.ts
Normal file
133
dlt-connector/src/data/proto/3_3/TransactionBody.ts
Normal file
@ -0,0 +1,133 @@
|
||||
import { Transaction } from '@entity/Transaction'
|
||||
import { Field, Message, OneOf } from 'protobufjs'
|
||||
|
||||
import { CommunityDraft } from '@/graphql/input/CommunityDraft'
|
||||
import { TransactionDraft } from '@/graphql/input/TransactionDraft'
|
||||
import { LogError } from '@/server/LogError'
|
||||
import { timestampToDate } from '@/utils/typeConverter'
|
||||
|
||||
import { AbstractTransaction } from '../AbstractTransaction'
|
||||
|
||||
import { CommunityRoot } from './CommunityRoot'
|
||||
import { PROTO_TRANSACTION_BODY_VERSION_NUMBER } from './const'
|
||||
import { CrossGroupType } from './enum/CrossGroupType'
|
||||
import { TransactionType } from './enum/TransactionType'
|
||||
import { GradidoCreation } from './GradidoCreation'
|
||||
import { GradidoDeferredTransfer } from './GradidoDeferredTransfer'
|
||||
import { GradidoTransfer } from './GradidoTransfer'
|
||||
import { GroupFriendsUpdate } from './GroupFriendsUpdate'
|
||||
import { RegisterAddress } from './RegisterAddress'
|
||||
import { Timestamp } from './Timestamp'
|
||||
|
||||
// https://www.npmjs.com/package/@apollo/protobufjs
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
export class TransactionBody extends Message<TransactionBody> {
|
||||
public constructor(transaction?: TransactionDraft | CommunityDraft) {
|
||||
if (transaction) {
|
||||
super({
|
||||
memo: 'Not implemented yet',
|
||||
createdAt: new Timestamp(new Date(transaction.createdAt)),
|
||||
versionNumber: PROTO_TRANSACTION_BODY_VERSION_NUMBER,
|
||||
type: CrossGroupType.LOCAL,
|
||||
otherGroup: '',
|
||||
})
|
||||
} else {
|
||||
super()
|
||||
}
|
||||
}
|
||||
|
||||
@Field.d(1, 'string')
|
||||
public memo: string
|
||||
|
||||
@Field.d(2, Timestamp)
|
||||
public createdAt: Timestamp
|
||||
|
||||
@Field.d(3, 'string')
|
||||
public versionNumber: string
|
||||
|
||||
@Field.d(4, CrossGroupType)
|
||||
public type: CrossGroupType
|
||||
|
||||
@Field.d(5, 'string')
|
||||
public otherGroup: string
|
||||
|
||||
@OneOf.d(
|
||||
'gradidoTransfer',
|
||||
'gradidoCreation',
|
||||
'groupFriendsUpdate',
|
||||
'registerAddress',
|
||||
'gradidoDeferredTransfer',
|
||||
'communityRoot',
|
||||
)
|
||||
public data: string
|
||||
|
||||
@Field.d(6, 'GradidoTransfer')
|
||||
transfer?: GradidoTransfer
|
||||
|
||||
@Field.d(7, 'GradidoCreation')
|
||||
creation?: GradidoCreation
|
||||
|
||||
@Field.d(8, 'GroupFriendsUpdate')
|
||||
groupFriendsUpdate?: GroupFriendsUpdate
|
||||
|
||||
@Field.d(9, 'RegisterAddress')
|
||||
registerAddress?: RegisterAddress
|
||||
|
||||
@Field.d(10, 'GradidoDeferredTransfer')
|
||||
deferredTransfer?: GradidoDeferredTransfer
|
||||
|
||||
@Field.d(11, 'CommunityRoot')
|
||||
communityRoot?: CommunityRoot
|
||||
|
||||
public getTransactionType(): TransactionType | undefined {
|
||||
if (this.transfer) return TransactionType.GRADIDO_TRANSFER
|
||||
else if (this.creation) return TransactionType.GRADIDO_CREATION
|
||||
else if (this.groupFriendsUpdate) return TransactionType.GROUP_FRIENDS_UPDATE
|
||||
else if (this.registerAddress) return TransactionType.REGISTER_ADDRESS
|
||||
else if (this.deferredTransfer) return TransactionType.GRADIDO_DEFERRED_TRANSFER
|
||||
else if (this.communityRoot) return TransactionType.COMMUNITY_ROOT
|
||||
}
|
||||
|
||||
// The `TransactionBody` class utilizes Protobuf's `OneOf` field structure which, according to Protobuf documentation
|
||||
// (https://protobuf.dev/programming-guides/proto3/#oneof), allows only one field within the group to be set at a time.
|
||||
// Therefore, accessing the `getTransactionDetails()` method returns the first initialized value among the defined fields,
|
||||
// each of which should be of type AbstractTransaction. It's important to note that due to the nature of Protobuf's `OneOf`,
|
||||
// only one type from the defined options can be set within the object obtained from Protobuf.
|
||||
//
|
||||
// If multiple fields are set in a single object, the method `getTransactionDetails()` will return the first defined value
|
||||
// based on the order of checks. Developers should handle this behavior according to the expected Protobuf structure.
|
||||
public getTransactionDetails(): AbstractTransaction | undefined {
|
||||
if (this.transfer) return this.transfer
|
||||
if (this.creation) return this.creation
|
||||
if (this.groupFriendsUpdate) return this.groupFriendsUpdate
|
||||
if (this.registerAddress) return this.registerAddress
|
||||
if (this.deferredTransfer) return this.deferredTransfer
|
||||
if (this.communityRoot) return this.communityRoot
|
||||
}
|
||||
|
||||
public fillTransactionRecipe(recipe: Transaction): void {
|
||||
recipe.createdAt = timestampToDate(this.createdAt)
|
||||
recipe.protocolVersion = this.versionNumber
|
||||
const transactionType = this.getTransactionType()
|
||||
if (!transactionType) {
|
||||
throw new LogError("invalid TransactionBody couldn't determine transaction type")
|
||||
}
|
||||
recipe.type = transactionType.valueOf()
|
||||
this.getTransactionDetails()?.fillTransactionRecipe(recipe)
|
||||
}
|
||||
|
||||
public getRecipientPublicKey(): Buffer | undefined {
|
||||
if (this.transfer) {
|
||||
// this.transfer.recipient contains the publicKey of the recipient
|
||||
return this.transfer.recipient
|
||||
}
|
||||
if (this.creation) {
|
||||
return this.creation.recipient.pubkey
|
||||
}
|
||||
if (this.deferredTransfer) {
|
||||
// this.deferredTransfer.transfer.recipient contains the publicKey of the recipient
|
||||
return this.deferredTransfer.transfer.recipient
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
import { Field, Message } from '@apollo/protobufjs'
|
||||
import { Field, Message } from 'protobufjs'
|
||||
|
||||
// https://www.npmjs.com/package/@apollo/protobufjs
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
1
dlt-connector/src/data/proto/3_3/const.ts
Normal file
1
dlt-connector/src/data/proto/3_3/const.ts
Normal file
@ -0,0 +1 @@
|
||||
export const PROTO_TRANSACTION_BODY_VERSION_NUMBER = '3.3'
|
||||
@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Enum for protobuf
|
||||
* used from RegisterAddress to determine account type
|
||||
* master implementation: https://github.com/gradido/gradido_protocol/blob/master/proto/gradido/register_address.proto
|
||||
*/
|
||||
export enum AddressType {
|
||||
NONE = 0, // if no address was found
|
||||
COMMUNITY_HUMAN = 1, // creation account for human
|
||||
22
dlt-connector/src/data/proto/3_3/enum/CrossGroupType.ts
Normal file
22
dlt-connector/src/data/proto/3_3/enum/CrossGroupType.ts
Normal file
@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Enum for protobuf
|
||||
* Determine Cross Group type of Transactions
|
||||
* LOCAL: no cross group transactions, sender and recipient community are the same, only one transaction
|
||||
* INBOUND: cross group transaction, Inbound part. On recipient community chain. Recipient side by Transfer Transactions
|
||||
* OUTBOUND: cross group transaction, Outbound part. On sender community chain. Sender side by Transfer Transactions
|
||||
* CROSS: for cross group transaction which haven't a direction like group friend update
|
||||
* master implementation: https://github.com/gradido/gradido_protocol/blob/master/proto/gradido/transaction_body.proto
|
||||
*
|
||||
* Transaction Handling differ from database focused backend
|
||||
* In Backend for each transfer transaction there are always two entries in db,
|
||||
* on for sender user and one for recipient user despite storing basically the same data two times
|
||||
* In Blockchain Implementation there only two transactions on cross group transactions, one for
|
||||
* the sender community chain, one for the recipient community chain
|
||||
* if the transaction stay in the community there is only one transaction
|
||||
*/
|
||||
export enum CrossGroupType {
|
||||
LOCAL = 0,
|
||||
INBOUND = 1,
|
||||
OUTBOUND = 2,
|
||||
CROSS = 3,
|
||||
}
|
||||
13
dlt-connector/src/data/proto/3_3/enum/TransactionType.ts
Normal file
13
dlt-connector/src/data/proto/3_3/enum/TransactionType.ts
Normal file
@ -0,0 +1,13 @@
|
||||
/**
|
||||
* based on TransactionBody data oneOf
|
||||
* https://github.com/gradido/gradido_protocol/blob/master/proto/gradido/transaction_body.proto
|
||||
* for storing type in db as number
|
||||
*/
|
||||
export enum TransactionType {
|
||||
GRADIDO_TRANSFER = 1,
|
||||
GRADIDO_CREATION = 2,
|
||||
GROUP_FRIENDS_UPDATE = 3,
|
||||
REGISTER_ADDRESS = 4,
|
||||
GRADIDO_DEFERRED_TRANSFER = 5,
|
||||
COMMUNITY_ROOT = 6,
|
||||
}
|
||||
5
dlt-connector/src/data/proto/AbstractTransaction.ts
Normal file
5
dlt-connector/src/data/proto/AbstractTransaction.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { Transaction } from '@entity/Transaction'
|
||||
|
||||
export abstract class AbstractTransaction {
|
||||
public abstract fillTransactionRecipe(recipe: Transaction): void
|
||||
}
|
||||
138
dlt-connector/src/data/proto/TransactionBody.builder.ts
Normal file
138
dlt-connector/src/data/proto/TransactionBody.builder.ts
Normal file
@ -0,0 +1,138 @@
|
||||
import { Account } from '@entity/Account'
|
||||
import { Community } from '@entity/Community'
|
||||
|
||||
import { InputTransactionType } from '@/graphql/enum/InputTransactionType'
|
||||
import { CommunityDraft } from '@/graphql/input/CommunityDraft'
|
||||
import { TransactionDraft } from '@/graphql/input/TransactionDraft'
|
||||
import { LogError } from '@/server/LogError'
|
||||
|
||||
import { CommunityRoot } from './3_3/CommunityRoot'
|
||||
import { CrossGroupType } from './3_3/enum/CrossGroupType'
|
||||
import { GradidoCreation } from './3_3/GradidoCreation'
|
||||
import { GradidoTransfer } from './3_3/GradidoTransfer'
|
||||
import { TransactionBody } from './3_3/TransactionBody'
|
||||
|
||||
export class TransactionBodyBuilder {
|
||||
private signingAccount?: Account
|
||||
private recipientAccount?: Account
|
||||
private body: TransactionBody | undefined
|
||||
|
||||
// https://refactoring.guru/design-patterns/builder/typescript/example
|
||||
/**
|
||||
* A fresh builder instance should contain a blank product object, which is
|
||||
* used in further assembly.
|
||||
*/
|
||||
constructor() {
|
||||
this.reset()
|
||||
}
|
||||
|
||||
public reset(): void {
|
||||
this.body = undefined
|
||||
this.signingAccount = undefined
|
||||
this.recipientAccount = undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Concrete Builders are supposed to provide their own methods for
|
||||
* retrieving results. That's because various types of builders may create
|
||||
* entirely different products that don't follow the same interface.
|
||||
* Therefore, such methods cannot be declared in the base Builder interface
|
||||
* (at least in a statically typed programming language).
|
||||
*
|
||||
* Usually, after returning the end result to the client, a builder instance
|
||||
* is expected to be ready to start producing another product. That's why
|
||||
* it's a usual practice to call the reset method at the end of the
|
||||
* `getProduct` method body. However, this behavior is not mandatory, and
|
||||
* you can make your builders wait for an explicit reset call from the
|
||||
* client code before disposing of the previous result.
|
||||
*/
|
||||
public build(): TransactionBody {
|
||||
const result = this.getTransactionBody()
|
||||
this.reset()
|
||||
return result
|
||||
}
|
||||
|
||||
public getTransactionBody(): TransactionBody {
|
||||
if (!this.body) {
|
||||
throw new LogError(
|
||||
'cannot build Transaction Body, missing information, please call at least fromTransactionDraft or fromCommunityDraft',
|
||||
)
|
||||
}
|
||||
return this.body
|
||||
}
|
||||
|
||||
public getSigningAccount(): Account | undefined {
|
||||
return this.signingAccount
|
||||
}
|
||||
|
||||
public getRecipientAccount(): Account | undefined {
|
||||
return this.recipientAccount
|
||||
}
|
||||
|
||||
public setSigningAccount(signingAccount: Account): TransactionBodyBuilder {
|
||||
this.signingAccount = signingAccount
|
||||
return this
|
||||
}
|
||||
|
||||
public setRecipientAccount(recipientAccount: Account): TransactionBodyBuilder {
|
||||
this.recipientAccount = recipientAccount
|
||||
return this
|
||||
}
|
||||
|
||||
public setCrossGroupType(type: CrossGroupType): this {
|
||||
if (!this.body) {
|
||||
throw new LogError(
|
||||
'body is undefined, please call fromTransactionDraft or fromCommunityDraft before',
|
||||
)
|
||||
}
|
||||
this.body.type = type
|
||||
return this
|
||||
}
|
||||
|
||||
public setOtherGroup(otherGroup: string): this {
|
||||
if (!this.body) {
|
||||
throw new LogError(
|
||||
'body is undefined, please call fromTransactionDraft or fromCommunityDraft before',
|
||||
)
|
||||
}
|
||||
this.body.otherGroup = otherGroup
|
||||
return this
|
||||
}
|
||||
|
||||
public fromTransactionDraft(transactionDraft: TransactionDraft): TransactionBodyBuilder {
|
||||
this.body = new TransactionBody(transactionDraft)
|
||||
// TODO: load pubkeys for sender and recipient user from db
|
||||
switch (transactionDraft.type) {
|
||||
case InputTransactionType.CREATION:
|
||||
if (!this.recipientAccount) {
|
||||
throw new LogError('missing recipient account for creation transaction!')
|
||||
}
|
||||
this.body.creation = new GradidoCreation(transactionDraft, this.recipientAccount)
|
||||
this.body.data = 'gradidoCreation'
|
||||
break
|
||||
case InputTransactionType.SEND:
|
||||
case InputTransactionType.RECEIVE:
|
||||
if (!this.recipientAccount || !this.signingAccount) {
|
||||
throw new LogError('missing signing and/or recipient account for transfer transaction!')
|
||||
}
|
||||
this.body.transfer = new GradidoTransfer(
|
||||
transactionDraft,
|
||||
this.signingAccount,
|
||||
this.recipientAccount,
|
||||
)
|
||||
this.body.data = 'gradidoTransfer'
|
||||
break
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
public fromCommunityDraft(
|
||||
communityDraft: CommunityDraft,
|
||||
community: Community,
|
||||
): TransactionBodyBuilder {
|
||||
this.body = new TransactionBody(communityDraft)
|
||||
this.body.communityRoot = new CommunityRoot(community)
|
||||
this.body.data = 'communityRoot'
|
||||
return this
|
||||
}
|
||||
}
|
||||
19
dlt-connector/src/graphql/arg/CommunityArg.ts
Normal file
19
dlt-connector/src/graphql/arg/CommunityArg.ts
Normal file
@ -0,0 +1,19 @@
|
||||
// https://www.npmjs.com/package/@apollo/protobufjs
|
||||
|
||||
import { IsBoolean, IsUUID } from 'class-validator'
|
||||
import { ArgsType, Field } from 'type-graphql'
|
||||
|
||||
@ArgsType()
|
||||
export class CommunityArg {
|
||||
@Field(() => String, { nullable: true })
|
||||
@IsUUID('4')
|
||||
uuid?: string
|
||||
|
||||
@Field(() => Boolean, { nullable: true })
|
||||
@IsBoolean()
|
||||
foreign?: boolean
|
||||
|
||||
@Field(() => Boolean, { nullable: true })
|
||||
@IsBoolean()
|
||||
confirmed?: boolean
|
||||
}
|
||||
21
dlt-connector/src/graphql/enum/AccountType.ts
Normal file
21
dlt-connector/src/graphql/enum/AccountType.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { registerEnumType } from 'type-graphql'
|
||||
|
||||
/**
|
||||
* enum for graphql
|
||||
* describe input account type in UserAccountDraft
|
||||
* should have the same entries like enum AddressType from proto/enum folder
|
||||
*/
|
||||
export enum AccountType {
|
||||
NONE = 'NONE', // if no address was found
|
||||
COMMUNITY_HUMAN = 'COMMUNITY_HUMAN', // creation account for human
|
||||
COMMUNITY_GMW = 'COMMUNITY_GMW', // community public budget account
|
||||
COMMUNITY_AUF = 'COMMUNITY_AUF', // community compensation and environment founds account
|
||||
COMMUNITY_PROJECT = 'COMMUNITY_PROJECT', // no creations allowed
|
||||
SUBACCOUNT = 'SUBACCOUNT', // no creations allowed
|
||||
CRYPTO_ACCOUNT = 'CRYPTO_ACCOUNT', // user control his keys, no creations
|
||||
}
|
||||
|
||||
registerEnumType(AccountType, {
|
||||
name: 'AccountType', // this one is mandatory
|
||||
description: 'Type of account', // this one is optional
|
||||
})
|
||||
@ -1,7 +0,0 @@
|
||||
export enum CrossGroupType {
|
||||
LOCAL = 0,
|
||||
INBOUND = 1,
|
||||
OUTBOUND = 2,
|
||||
// for cross group transaction which haven't a direction like group friend update
|
||||
// CROSS = 3,
|
||||
}
|
||||
14
dlt-connector/src/graphql/enum/InputTransactionType.ts
Executable file
14
dlt-connector/src/graphql/enum/InputTransactionType.ts
Executable file
@ -0,0 +1,14 @@
|
||||
import { registerEnumType } from 'type-graphql'
|
||||
|
||||
// enum for graphql but with int because it is the same in backend
|
||||
// for transaction type from backend
|
||||
export enum InputTransactionType {
|
||||
CREATION = 1,
|
||||
SEND = 2,
|
||||
RECEIVE = 3,
|
||||
}
|
||||
|
||||
registerEnumType(InputTransactionType, {
|
||||
name: 'InputTransactionType', // this one is mandatory
|
||||
description: 'Type of the transaction', // this one is optional
|
||||
})
|
||||
@ -1,10 +1,17 @@
|
||||
import { registerEnumType } from 'type-graphql'
|
||||
|
||||
// enum for graphql
|
||||
// error groups for resolver answers
|
||||
export enum TransactionErrorType {
|
||||
NOT_IMPLEMENTED_YET = 'Not Implemented yet',
|
||||
MISSING_PARAMETER = 'Missing parameter',
|
||||
ALREADY_EXIST = 'Already exist',
|
||||
DB_ERROR = 'DB Error',
|
||||
PROTO_DECODE_ERROR = 'Proto Decode Error',
|
||||
PROTO_ENCODE_ERROR = 'Proto Encode Error',
|
||||
INVALID_SIGNATURE = 'Invalid Signature',
|
||||
LOGIC_ERROR = 'Logic Error',
|
||||
NOT_FOUND = 'Not found',
|
||||
}
|
||||
|
||||
registerEnumType(TransactionErrorType, {
|
||||
|
||||
@ -1,12 +0,0 @@
|
||||
import { registerEnumType } from 'type-graphql'
|
||||
|
||||
export enum TransactionType {
|
||||
CREATION = 1,
|
||||
SEND = 2,
|
||||
RECEIVE = 3,
|
||||
}
|
||||
|
||||
registerEnumType(TransactionType, {
|
||||
name: 'TransactionType', // this one is mandatory
|
||||
description: 'Type of the transaction', // this one is optional
|
||||
})
|
||||
@ -1,15 +0,0 @@
|
||||
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',
|
||||
})
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
import { IsBoolean, IsUUID } from 'class-validator'
|
||||
import { Field, InputType } from 'type-graphql'
|
||||
|
||||
import { isValidDateString } from '@validator/DateString'
|
||||
|
||||
@InputType()
|
||||
|
||||
@ -1,32 +1,37 @@
|
||||
// https://www.npmjs.com/package/@apollo/protobufjs
|
||||
|
||||
import { IsEnum, IsObject, IsPositive, ValidateNested } from 'class-validator'
|
||||
import { Decimal } from 'decimal.js-light'
|
||||
import { TransactionType } from '@enum/TransactionType'
|
||||
import { InputType, Field } from 'type-graphql'
|
||||
import { UserIdentifier } from './UserIdentifier'
|
||||
import { InputType, Field, Int } from 'type-graphql'
|
||||
|
||||
import { InputTransactionType } from '@enum/InputTransactionType'
|
||||
import { isValidDateString } from '@validator/DateString'
|
||||
import { IsPositiveDecimal } from '@validator/Decimal'
|
||||
import { IsEnum, IsObject, ValidateNested } from 'class-validator'
|
||||
|
||||
import { UserIdentifier } from './UserIdentifier'
|
||||
|
||||
@InputType()
|
||||
export class TransactionDraft {
|
||||
@Field(() => UserIdentifier)
|
||||
@IsObject()
|
||||
@ValidateNested()
|
||||
senderUser: UserIdentifier
|
||||
user: UserIdentifier
|
||||
|
||||
@Field(() => UserIdentifier)
|
||||
@IsObject()
|
||||
@ValidateNested()
|
||||
recipientUser: UserIdentifier
|
||||
linkedUser: UserIdentifier
|
||||
|
||||
@Field(() => Int)
|
||||
@IsPositive()
|
||||
backendTransactionId: number
|
||||
|
||||
@Field(() => Decimal)
|
||||
@IsPositiveDecimal()
|
||||
amount: Decimal
|
||||
|
||||
@Field(() => TransactionType)
|
||||
@IsEnum(TransactionType)
|
||||
type: TransactionType
|
||||
@Field(() => InputTransactionType)
|
||||
@IsEnum(InputTransactionType)
|
||||
type: InputTransactionType
|
||||
|
||||
@Field(() => String)
|
||||
@isValidDateString()
|
||||
|
||||
@ -1,27 +0,0 @@
|
||||
// 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 { 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')
|
||||
// @Field(() => Decimal)
|
||||
// communitySum: Decimal
|
||||
}
|
||||
26
dlt-connector/src/graphql/input/UserAccountDraft.ts
Normal file
26
dlt-connector/src/graphql/input/UserAccountDraft.ts
Normal file
@ -0,0 +1,26 @@
|
||||
// https://www.npmjs.com/package/@apollo/protobufjs
|
||||
|
||||
import { IsEnum, IsObject, ValidateNested } from 'class-validator'
|
||||
import { InputType, Field } from 'type-graphql'
|
||||
|
||||
import { isValidDateString } from '@validator/DateString'
|
||||
|
||||
import { AccountType } from '@/graphql/enum/AccountType'
|
||||
|
||||
import { UserIdentifier } from './UserIdentifier'
|
||||
|
||||
@InputType()
|
||||
export class UserAccountDraft {
|
||||
@Field(() => UserIdentifier)
|
||||
@IsObject()
|
||||
@ValidateNested()
|
||||
user: UserIdentifier
|
||||
|
||||
@Field(() => String)
|
||||
@isValidDateString()
|
||||
createdAt: string
|
||||
|
||||
@Field(() => AccountType)
|
||||
@IsEnum(AccountType)
|
||||
accountType: AccountType
|
||||
}
|
||||
36
dlt-connector/src/graphql/model/Community.ts
Normal file
36
dlt-connector/src/graphql/model/Community.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import { Community as CommunityEntity } from '@entity/Community'
|
||||
import { ObjectType, Field, Int } from 'type-graphql'
|
||||
|
||||
@ObjectType()
|
||||
export class Community {
|
||||
constructor(entity: CommunityEntity) {
|
||||
this.id = entity.id
|
||||
this.iotaTopic = entity.iotaTopic
|
||||
if (entity.rootPubkey) {
|
||||
this.rootPublicKeyHex = entity.rootPubkey?.toString('hex')
|
||||
}
|
||||
this.foreign = entity.foreign
|
||||
this.createdAt = entity.createdAt.toString()
|
||||
if (entity.confirmedAt) {
|
||||
this.confirmedAt = entity.confirmedAt.toString()
|
||||
}
|
||||
}
|
||||
|
||||
@Field(() => Int)
|
||||
id: number
|
||||
|
||||
@Field(() => String)
|
||||
iotaTopic: string
|
||||
|
||||
@Field(() => String)
|
||||
rootPublicKeyHex?: string
|
||||
|
||||
@Field(() => Boolean)
|
||||
foreign: boolean
|
||||
|
||||
@Field(() => String)
|
||||
createdAt: string
|
||||
|
||||
@Field(() => String)
|
||||
confirmedAt?: string
|
||||
}
|
||||
@ -1,4 +1,5 @@
|
||||
import { ObjectType, Field } from 'type-graphql'
|
||||
|
||||
import { TransactionErrorType } from '../enum/TransactionErrorType'
|
||||
|
||||
@ObjectType()
|
||||
|
||||
32
dlt-connector/src/graphql/model/TransactionRecipe.ts
Normal file
32
dlt-connector/src/graphql/model/TransactionRecipe.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { Transaction } from '@entity/Transaction'
|
||||
import { Field, Int, ObjectType } from 'type-graphql'
|
||||
|
||||
import { TransactionType } from '@/data/proto/3_3/enum/TransactionType'
|
||||
import { LogError } from '@/server/LogError'
|
||||
import { getEnumValue } from '@/utils/typeConverter'
|
||||
|
||||
@ObjectType()
|
||||
export class TransactionRecipe {
|
||||
public constructor({ id, createdAt, type, community }: Transaction) {
|
||||
const transactionType = getEnumValue(TransactionType, type)
|
||||
if (!transactionType) {
|
||||
throw new LogError('invalid transaction, type is missing')
|
||||
}
|
||||
this.id = id
|
||||
this.createdAt = createdAt.toString()
|
||||
this.type = transactionType.toString()
|
||||
this.topic = community.iotaTopic
|
||||
}
|
||||
|
||||
@Field(() => Int)
|
||||
id: number
|
||||
|
||||
@Field(() => String)
|
||||
createdAt: string
|
||||
|
||||
@Field(() => String)
|
||||
type: string
|
||||
|
||||
@Field(() => String)
|
||||
topic: string
|
||||
}
|
||||
@ -1,15 +1,17 @@
|
||||
import { ObjectType, Field } from 'type-graphql'
|
||||
|
||||
import { TransactionError } from './TransactionError'
|
||||
import { TransactionRecipe } from './TransactionRecipe'
|
||||
|
||||
@ObjectType()
|
||||
export class TransactionResult {
|
||||
constructor(content?: TransactionError | string) {
|
||||
constructor(content?: TransactionError | TransactionRecipe) {
|
||||
this.succeed = true
|
||||
if (content instanceof TransactionError) {
|
||||
this.error = content
|
||||
this.succeed = false
|
||||
} else if (typeof content === 'string') {
|
||||
this.messageId = content
|
||||
} else if (content instanceof TransactionRecipe) {
|
||||
this.recipe = content
|
||||
}
|
||||
}
|
||||
|
||||
@ -18,8 +20,8 @@ export class TransactionResult {
|
||||
error?: TransactionError
|
||||
|
||||
// if no error happend, the message id of the iota transaction
|
||||
@Field(() => String, { nullable: true })
|
||||
messageId?: string
|
||||
@Field(() => TransactionRecipe, { nullable: true })
|
||||
recipe?: TransactionRecipe
|
||||
|
||||
@Field(() => Boolean)
|
||||
succeed: boolean
|
||||
|
||||
@ -1,34 +1,40 @@
|
||||
import 'reflect-metadata'
|
||||
import { ApolloServer } from '@apollo/server'
|
||||
import { createApolloTestServer } from '@test/ApolloServerMock'
|
||||
import assert from 'assert'
|
||||
|
||||
import { ApolloServer } from '@apollo/server'
|
||||
|
||||
// must be imported before createApolloTestServer so that TestDB was created before createApolloTestServer imports repositories
|
||||
// eslint-disable-next-line import/order
|
||||
import { TestDB } from '@test/TestDB'
|
||||
import { TransactionResult } from '../model/TransactionResult'
|
||||
import { TransactionResult } from '@model/TransactionResult'
|
||||
import { createApolloTestServer } from '@test/ApolloServerMock'
|
||||
|
||||
import { CONFIG } from '@/config'
|
||||
|
||||
CONFIG.IOTA_HOME_COMMUNITY_SEED = 'aabbccddeeff00112233445566778899aabbccddeeff00112233445566778899'
|
||||
|
||||
const con = TestDB.instance
|
||||
|
||||
jest.mock('@typeorm/DataSource', () => ({
|
||||
getDataSource: jest.fn(() => TestDB.instance.dbConnect),
|
||||
}))
|
||||
|
||||
let apolloTestServer: ApolloServer
|
||||
|
||||
jest.mock('@typeorm/DataSource', () => ({
|
||||
getDataSource: () => TestDB.instance.dbConnect,
|
||||
}))
|
||||
|
||||
describe('graphql/resolver/CommunityResolver', () => {
|
||||
beforeAll(async () => {
|
||||
await con.setupTestDB()
|
||||
apolloTestServer = await createApolloTestServer()
|
||||
})
|
||||
afterAll(async () => {
|
||||
await con.teardownTestDB()
|
||||
})
|
||||
|
||||
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} }',
|
||||
query:
|
||||
'mutation ($input: CommunityDraft!) { addCommunity(data: $input) {succeed, error {message}} }',
|
||||
variables: {
|
||||
input: {
|
||||
uuid: '3d813cbb-37fb-42ba-91df-831e1593ac29',
|
||||
@ -45,7 +51,8 @@ describe('graphql/resolver/CommunityResolver', () => {
|
||||
|
||||
it('test add home community', async () => {
|
||||
const response = await apolloTestServer.executeOperation({
|
||||
query: 'mutation ($input: CommunityDraft!) { addCommunity(data: $input) {succeed} }',
|
||||
query:
|
||||
'mutation ($input: CommunityDraft!) { addCommunity(data: $input) {succeed, error {message}} }',
|
||||
variables: {
|
||||
input: {
|
||||
uuid: '3d823cad-37fb-41cd-91df-152e1593ac29',
|
||||
|
||||
@ -1,47 +1,67 @@
|
||||
import { Resolver, Arg, Mutation } from 'type-graphql'
|
||||
import { Resolver, Query, Arg, Mutation, Args } from 'type-graphql'
|
||||
|
||||
import { CommunityArg } from '@arg/CommunityArg'
|
||||
import { TransactionErrorType } from '@enum/TransactionErrorType'
|
||||
import { CommunityDraft } from '@input/CommunityDraft'
|
||||
import { Community } from '@model/Community'
|
||||
import { TransactionError } from '@model/TransactionError'
|
||||
import { TransactionResult } from '@model/TransactionResult'
|
||||
|
||||
import { TransactionResult } from '../model/TransactionResult'
|
||||
import { TransactionError } from '../model/TransactionError'
|
||||
import { create as createCommunity, isExist } from '@/controller/Community'
|
||||
import { TransactionErrorType } from '../enum/TransactionErrorType'
|
||||
import { CommunityRepository } from '@/data/Community.repository'
|
||||
import { AddCommunityContext } from '@/interactions/backendToDb/community/AddCommunity.context'
|
||||
import { LogError } from '@/server/LogError'
|
||||
import { logger } from '@/server/logger'
|
||||
import { iotaTopicFromCommunityUUID } from '@/utils/typeConverter'
|
||||
|
||||
@Resolver()
|
||||
export class CommunityResolver {
|
||||
@Query(() => Community)
|
||||
async community(@Args() communityArg: CommunityArg): Promise<Community> {
|
||||
logger.info('community', communityArg)
|
||||
const result = await CommunityRepository.findByCommunityArg(communityArg)
|
||||
if (result.length === 0) {
|
||||
throw new LogError('cannot find community')
|
||||
} else if (result.length === 1) {
|
||||
return new Community(result[0])
|
||||
} else {
|
||||
throw new LogError('find multiple communities')
|
||||
}
|
||||
}
|
||||
|
||||
@Query(() => Boolean)
|
||||
async isCommunityExist(@Args() communityArg: CommunityArg): Promise<boolean> {
|
||||
logger.info('isCommunity', communityArg)
|
||||
return (await CommunityRepository.findByCommunityArg(communityArg)).length === 1
|
||||
}
|
||||
|
||||
@Query(() => [Community])
|
||||
async communities(@Args() communityArg: CommunityArg): Promise<Community[]> {
|
||||
logger.info('communities', communityArg)
|
||||
const result = await CommunityRepository.findByCommunityArg(communityArg)
|
||||
return result.map((communityEntity) => new Community(communityEntity))
|
||||
}
|
||||
|
||||
@Mutation(() => TransactionResult)
|
||||
async addCommunity(
|
||||
@Arg('data')
|
||||
communityDraft: CommunityDraft,
|
||||
): Promise<TransactionResult> {
|
||||
logger.info('addCommunity', communityDraft)
|
||||
const topic = iotaTopicFromCommunityUUID(communityDraft.uuid)
|
||||
// check if community was already written to db
|
||||
if (await CommunityRepository.isExist(topic)) {
|
||||
return new TransactionResult(
|
||||
new TransactionError(TransactionErrorType.ALREADY_EXIST, 'community already exist!'),
|
||||
)
|
||||
}
|
||||
// prepare context for interaction
|
||||
// shouldn't throw at all
|
||||
// TODO: write tests to make sure that it doesn't throw
|
||||
const addCommunityContext = new AddCommunityContext(communityDraft, topic)
|
||||
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
|
||||
// actually run interaction, create community, accounts for foreign community and transactionRecipe
|
||||
await addCommunityContext.run()
|
||||
return new TransactionResult()
|
||||
} catch (error) {
|
||||
if (error instanceof TransactionError) {
|
||||
return new TransactionResult(error)
|
||||
|
||||
@ -1,8 +1,28 @@
|
||||
import 'reflect-metadata'
|
||||
import { ApolloServer } from '@apollo/server'
|
||||
import { createApolloTestServer } from '@test/ApolloServerMock'
|
||||
import assert from 'assert'
|
||||
import { TransactionResult } from '../model/TransactionResult'
|
||||
|
||||
import { ApolloServer } from '@apollo/server'
|
||||
|
||||
// must be imported before createApolloTestServer so that TestDB was created before createApolloTestServer imports repositories
|
||||
// eslint-disable-next-line import/order
|
||||
import { TestDB } from '@test/TestDB'
|
||||
import { AccountType } from '@enum/AccountType'
|
||||
import { TransactionResult } from '@model/TransactionResult'
|
||||
import { createApolloTestServer } from '@test/ApolloServerMock'
|
||||
|
||||
import { CONFIG } from '@/config'
|
||||
import { AccountFactory } from '@/data/Account.factory'
|
||||
import { KeyPair } from '@/data/KeyPair'
|
||||
import { Mnemonic } from '@/data/Mnemonic'
|
||||
import { UserFactory } from '@/data/User.factory'
|
||||
import { UserLogic } from '@/data/User.logic'
|
||||
import { AddCommunityContext } from '@/interactions/backendToDb/community/AddCommunity.context'
|
||||
import { getEnumValue } from '@/utils/typeConverter'
|
||||
|
||||
import { InputTransactionType } from '../enum/InputTransactionType'
|
||||
import { CommunityDraft } from '../input/CommunityDraft'
|
||||
import { UserAccountDraft } from '../input/UserAccountDraft'
|
||||
import { UserIdentifier } from '../input/UserIdentifier'
|
||||
|
||||
let apolloTestServer: ApolloServer
|
||||
|
||||
@ -14,63 +34,88 @@ jest.mock('@/client/IotaClient', () => {
|
||||
}
|
||||
})
|
||||
|
||||
jest.mock('@typeorm/DataSource', () => ({
|
||||
getDataSource: jest.fn(() => TestDB.instance.dbConnect),
|
||||
}))
|
||||
|
||||
CONFIG.IOTA_HOME_COMMUNITY_SEED = 'aabbccddeeff00112233445566778899aabbccddeeff00112233445566778899'
|
||||
const communityUUID = '3d813cbb-37fb-42ba-91df-831e1593ac29'
|
||||
const communityKeyPair = new KeyPair(new Mnemonic(CONFIG.IOTA_HOME_COMMUNITY_SEED))
|
||||
|
||||
const createUserStoreAccount = async (uuid: string): Promise<UserIdentifier> => {
|
||||
const userAccountDraft = new UserAccountDraft()
|
||||
userAccountDraft.accountType = AccountType.COMMUNITY_HUMAN
|
||||
userAccountDraft.createdAt = new Date().toString()
|
||||
userAccountDraft.user = new UserIdentifier()
|
||||
userAccountDraft.user.uuid = uuid
|
||||
userAccountDraft.user.communityUuid = communityUUID
|
||||
const user = UserFactory.create(userAccountDraft, communityKeyPair)
|
||||
const userLogic = new UserLogic(user)
|
||||
const account = AccountFactory.createAccountFromUserAccountDraft(
|
||||
userAccountDraft,
|
||||
userLogic.calculateKeyPair(communityKeyPair),
|
||||
)
|
||||
account.user = user
|
||||
// user is set to cascade: ['insert'] will be saved together with account
|
||||
await account.save()
|
||||
return userAccountDraft.user
|
||||
}
|
||||
|
||||
describe('Transaction Resolver Test', () => {
|
||||
let user: UserIdentifier
|
||||
let linkedUser: UserIdentifier
|
||||
beforeAll(async () => {
|
||||
await TestDB.instance.setupTestDB()
|
||||
apolloTestServer = await createApolloTestServer()
|
||||
|
||||
const communityDraft = new CommunityDraft()
|
||||
communityDraft.uuid = communityUUID
|
||||
communityDraft.foreign = false
|
||||
communityDraft.createdAt = new Date().toString()
|
||||
const addCommunityContext = new AddCommunityContext(communityDraft)
|
||||
await addCommunityContext.run()
|
||||
user = await createUserStoreAccount('0ec72b74-48c2-446f-91ce-31ad7d9f4d65')
|
||||
linkedUser = await createUserStoreAccount('ddc8258e-fcb5-4e48-8d1d-3a07ec371dbe')
|
||||
})
|
||||
it('test version query', async () => {
|
||||
const response = await apolloTestServer.executeOperation({
|
||||
query: '{ version }',
|
||||
})
|
||||
// Note the use of Node's assert rather than Jest's expect; if using
|
||||
// TypeScript, `assert`` will appropriately narrow the type of `body`
|
||||
// and `expect` will not.
|
||||
// Source: https://www.apollographql.com/docs/apollo-server/testing/testing
|
||||
assert(response.body.kind === 'single')
|
||||
expect(response.body.singleResult.errors).toBeUndefined()
|
||||
expect(response.body.singleResult.data?.version).toBe('0.1')
|
||||
|
||||
afterAll(async () => {
|
||||
await TestDB.instance.teardownTestDB()
|
||||
})
|
||||
|
||||
it('test mocked sendTransaction', async () => {
|
||||
const response = await apolloTestServer.executeOperation({
|
||||
query:
|
||||
'mutation ($input: TransactionDraft!) { sendTransaction(data: $input) {error {type, message}, messageId} }',
|
||||
'mutation ($input: TransactionDraft!) { sendTransaction(data: $input) {succeed, recipe { id, topic }} }',
|
||||
variables: {
|
||||
input: {
|
||||
senderUser: {
|
||||
uuid: '0ec72b74-48c2-446f-91ce-31ad7d9f4d65',
|
||||
},
|
||||
recipientUser: {
|
||||
uuid: 'ddc8258e-fcb5-4e48-8d1d-3a07ec371dbe',
|
||||
},
|
||||
type: 'SEND',
|
||||
user,
|
||||
linkedUser,
|
||||
type: getEnumValue(InputTransactionType, InputTransactionType.SEND),
|
||||
amount: '10',
|
||||
createdAt: '2012-04-17T17:12:00Z',
|
||||
backendTransactionId: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
assert(response.body.kind === 'single')
|
||||
expect(response.body.singleResult.errors).toBeUndefined()
|
||||
const transactionResult = response.body.singleResult.data?.sendTransaction as TransactionResult
|
||||
expect(transactionResult.messageId).toBe(
|
||||
'5498130bc3918e1a7143969ce05805502417e3e1bd596d3c44d6a0adeea22710',
|
||||
)
|
||||
expect(transactionResult.recipe).toBeDefined()
|
||||
expect(transactionResult.succeed).toBe(true)
|
||||
})
|
||||
|
||||
it('test mocked sendTransaction invalid transactionType ', async () => {
|
||||
const response = await apolloTestServer.executeOperation({
|
||||
query:
|
||||
'mutation ($input: TransactionDraft!) { sendTransaction(data: $input) {error {type, message}, messageId} }',
|
||||
'mutation ($input: TransactionDraft!) { sendTransaction(data: $input) {error {type, message}, succeed} }',
|
||||
variables: {
|
||||
input: {
|
||||
senderUser: {
|
||||
uuid: '0ec72b74-48c2-446f-91ce-31ad7d9f4d65',
|
||||
},
|
||||
recipientUser: {
|
||||
uuid: 'ddc8258e-fcb5-4e48-8d1d-3a07ec371dbe',
|
||||
},
|
||||
user,
|
||||
linkedUser,
|
||||
type: 'INVALID',
|
||||
amount: '10',
|
||||
createdAt: '2012-04-17T17:12:00Z',
|
||||
backendTransactionId: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
@ -79,7 +124,7 @@ describe('Transaction Resolver Test', () => {
|
||||
errors: [
|
||||
{
|
||||
message:
|
||||
'Variable "$input" got invalid value "INVALID" at "input.type"; Value "INVALID" does not exist in "TransactionType" enum.',
|
||||
'Variable "$input" got invalid value "INVALID" at "input.type"; Value "INVALID" does not exist in "InputTransactionType" enum.',
|
||||
},
|
||||
],
|
||||
})
|
||||
@ -88,18 +133,15 @@ describe('Transaction Resolver Test', () => {
|
||||
it('test mocked sendTransaction invalid amount ', async () => {
|
||||
const response = await apolloTestServer.executeOperation({
|
||||
query:
|
||||
'mutation ($input: TransactionDraft!) { sendTransaction(data: $input) {error {type, message}, messageId} }',
|
||||
'mutation ($input: TransactionDraft!) { sendTransaction(data: $input) {error {type, message}, succeed} }',
|
||||
variables: {
|
||||
input: {
|
||||
senderUser: {
|
||||
uuid: '0ec72b74-48c2-446f-91ce-31ad7d9f4d65',
|
||||
},
|
||||
recipientUser: {
|
||||
uuid: 'ddc8258e-fcb5-4e48-8d1d-3a07ec371dbe',
|
||||
},
|
||||
type: 'SEND',
|
||||
user,
|
||||
linkedUser,
|
||||
type: getEnumValue(InputTransactionType, InputTransactionType.SEND),
|
||||
amount: 'no number',
|
||||
createdAt: '2012-04-17T17:12:00Z',
|
||||
backendTransactionId: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
@ -117,18 +159,15 @@ describe('Transaction Resolver Test', () => {
|
||||
it('test mocked sendTransaction invalid created date ', async () => {
|
||||
const response = await apolloTestServer.executeOperation({
|
||||
query:
|
||||
'mutation ($input: TransactionDraft!) { sendTransaction(data: $input) {error {type, message}, messageId} }',
|
||||
'mutation ($input: TransactionDraft!) { sendTransaction(data: $input) {error {type, message}, succeed} }',
|
||||
variables: {
|
||||
input: {
|
||||
senderUser: {
|
||||
uuid: '0ec72b74-48c2-446f-91ce-31ad7d9f4d65',
|
||||
},
|
||||
recipientUser: {
|
||||
uuid: 'ddc8258e-fcb5-4e48-8d1d-3a07ec371dbe',
|
||||
},
|
||||
type: 'SEND',
|
||||
user,
|
||||
linkedUser,
|
||||
type: getEnumValue(InputTransactionType, InputTransactionType.SEND),
|
||||
amount: '10',
|
||||
createdAt: 'not valid',
|
||||
backendTransactionId: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
@ -156,18 +195,15 @@ describe('Transaction Resolver Test', () => {
|
||||
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} }',
|
||||
'mutation ($input: TransactionDraft!) { sendTransaction(data: $input) {error {type, message}, succeed} }',
|
||||
variables: {
|
||||
input: {
|
||||
senderUser: {
|
||||
uuid: '0ec72b74-48c2-446f-91ce-31ad7d9f4d65',
|
||||
},
|
||||
recipientUser: {
|
||||
uuid: 'ddc8258e-fcb5-4e48-8d1d-3a07ec371dbe',
|
||||
},
|
||||
type: 'CREATION',
|
||||
user,
|
||||
linkedUser,
|
||||
type: getEnumValue(InputTransactionType, InputTransactionType.CREATION),
|
||||
amount: '10',
|
||||
createdAt: '2012-04-17T17:12:00Z',
|
||||
backendTransactionId: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@ -1,40 +1,48 @@
|
||||
import { Resolver, Query, Arg, Mutation } from 'type-graphql'
|
||||
import { Resolver, Arg, Mutation } from 'type-graphql'
|
||||
|
||||
import { TransactionDraft } from '@input/TransactionDraft'
|
||||
|
||||
import { create as createTransactionBody } from '@controller/TransactionBody'
|
||||
import { create as createGradidoTransaction } from '@controller/GradidoTransaction'
|
||||
import { TransactionRepository } from '@/data/Transaction.repository'
|
||||
import { CreateTransactionRecipeContext } from '@/interactions/backendToDb/transaction/CreateTransationRecipe.context'
|
||||
import { LogError } from '@/server/LogError'
|
||||
|
||||
import { sendMessage as iotaSendMessage } from '@/client/IotaClient'
|
||||
import { GradidoTransaction } from '@/proto/3_3/GradidoTransaction'
|
||||
import { TransactionResult } from '../model/TransactionResult'
|
||||
import { TransactionError } from '../model/TransactionError'
|
||||
import { TransactionRecipe } from '../model/TransactionRecipe'
|
||||
import { TransactionResult } from '../model/TransactionResult'
|
||||
|
||||
@Resolver()
|
||||
export class TransactionResolver {
|
||||
// Why a dummy function?
|
||||
// to prevent this error by start:
|
||||
// GeneratingSchemaError: Some errors occurred while generating GraphQL schema:
|
||||
// Type Query must define one or more fields.
|
||||
// it seems that at least one query must be defined
|
||||
// https://github.com/ardatan/graphql-tools/issues/764
|
||||
@Query(() => String)
|
||||
version(): string {
|
||||
return '0.1'
|
||||
}
|
||||
|
||||
@Mutation(() => TransactionResult)
|
||||
async sendTransaction(
|
||||
@Arg('data')
|
||||
transaction: TransactionDraft,
|
||||
transactionDraft: TransactionDraft,
|
||||
): Promise<TransactionResult> {
|
||||
const createTransactionRecipeContext = new CreateTransactionRecipeContext(transactionDraft)
|
||||
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) {
|
||||
await createTransactionRecipeContext.run()
|
||||
const transactionRecipe = createTransactionRecipeContext.getTransactionRecipe()
|
||||
// check if a transaction with this signature already exist
|
||||
const existingRecipe = await TransactionRepository.findBySignature(
|
||||
transactionRecipe.signature,
|
||||
)
|
||||
if (existingRecipe) {
|
||||
// transaction recipe with this signature already exist, we need only to store the backendTransaction
|
||||
if (transactionRecipe.backendTransactions.length !== 1) {
|
||||
throw new LogError('unexpected backend transaction count', {
|
||||
count: transactionRecipe.backendTransactions.length,
|
||||
transactionId: transactionRecipe.id,
|
||||
})
|
||||
}
|
||||
const backendTransaction = transactionRecipe.backendTransactions[0]
|
||||
backendTransaction.transactionId = transactionRecipe.id
|
||||
await backendTransaction.save()
|
||||
} else {
|
||||
// we can store the transaction and with that automatic the backend transaction
|
||||
await transactionRecipe.save()
|
||||
}
|
||||
return new TransactionResult(new TransactionRecipe(transactionRecipe))
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (error: any) {
|
||||
if (error instanceof TransactionError) {
|
||||
return new TransactionResult(error)
|
||||
} else {
|
||||
|
||||
@ -2,14 +2,15 @@ import { Decimal } from 'decimal.js-light'
|
||||
import { GraphQLSchema } from 'graphql'
|
||||
import { buildSchema } from 'type-graphql'
|
||||
|
||||
import { DecimalScalar } from './scalar/Decimal'
|
||||
import { TransactionResolver } from './resolver/TransactionsResolver'
|
||||
import { CommunityResolver } from './resolver/CommunityResolver'
|
||||
import { TransactionResolver } from './resolver/TransactionsResolver'
|
||||
import { DecimalScalar } from './scalar/Decimal'
|
||||
|
||||
export const schema = async (): Promise<GraphQLSchema> => {
|
||||
return buildSchema({
|
||||
resolvers: [TransactionResolver, CommunityResolver],
|
||||
scalarsMap: [{ type: Decimal, scalar: DecimalScalar }],
|
||||
emitSchemaFile: true,
|
||||
validate: {
|
||||
validationError: { target: false },
|
||||
skipMissingProperties: true,
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { CONFIG } from '@/config'
|
||||
|
||||
import createServer from './server/createServer'
|
||||
|
||||
async function main() {
|
||||
|
||||
@ -0,0 +1,31 @@
|
||||
import { CommunityDraft } from '@/graphql/input/CommunityDraft'
|
||||
import { iotaTopicFromCommunityUUID } from '@/utils/typeConverter'
|
||||
|
||||
import { CommunityRole } from './Community.role'
|
||||
import { ForeignCommunityRole } from './ForeignCommunity.role'
|
||||
import { HomeCommunityRole } from './HomeCommunity.role'
|
||||
|
||||
/**
|
||||
* @DCI-Context
|
||||
* Context for adding community to DB
|
||||
* using roles to distinct between foreign and home communities
|
||||
*/
|
||||
export class AddCommunityContext {
|
||||
private communityRole: CommunityRole
|
||||
private iotaTopic: string
|
||||
public constructor(private communityDraft: CommunityDraft, iotaTopic?: string) {
|
||||
if (!iotaTopic) {
|
||||
this.iotaTopic = iotaTopicFromCommunityUUID(this.communityDraft.uuid)
|
||||
} else {
|
||||
this.iotaTopic = iotaTopic
|
||||
}
|
||||
this.communityRole = communityDraft.foreign
|
||||
? new ForeignCommunityRole()
|
||||
: new HomeCommunityRole()
|
||||
}
|
||||
|
||||
public async run(): Promise<void> {
|
||||
await this.communityRole.create(this.communityDraft, this.iotaTopic)
|
||||
await this.communityRole.store()
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
import { Community } from '@entity/Community'
|
||||
|
||||
import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType'
|
||||
import { CommunityDraft } from '@/graphql/input/CommunityDraft'
|
||||
import { TransactionError } from '@/graphql/model/TransactionError'
|
||||
import { logger } from '@/server/logger'
|
||||
|
||||
export abstract class CommunityRole {
|
||||
protected self: Community
|
||||
public constructor() {
|
||||
this.self = Community.create()
|
||||
}
|
||||
|
||||
public async create(communityDraft: CommunityDraft, topic: string): Promise<void> {
|
||||
this.self.iotaTopic = topic
|
||||
this.self.createdAt = new Date(communityDraft.createdAt)
|
||||
this.self.foreign = communityDraft.foreign
|
||||
}
|
||||
|
||||
public store(): Promise<Community> {
|
||||
try {
|
||||
return this.self.save()
|
||||
} catch (error) {
|
||||
logger.error('error saving new community into db: %s', error)
|
||||
throw new TransactionError(TransactionErrorType.DB_ERROR, 'error saving community into db')
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,4 @@
|
||||
import { CommunityRole } from './Community.role'
|
||||
|
||||
// same as base class
|
||||
export class ForeignCommunityRole extends CommunityRole {}
|
||||
@ -0,0 +1,51 @@
|
||||
import { Community } from '@entity/Community'
|
||||
import { Transaction } from '@entity/Transaction'
|
||||
|
||||
import { CONFIG } from '@/config'
|
||||
import { AccountFactory } from '@/data/Account.factory'
|
||||
import { KeyPair } from '@/data/KeyPair'
|
||||
import { Mnemonic } from '@/data/Mnemonic'
|
||||
import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType'
|
||||
import { CommunityDraft } from '@/graphql/input/CommunityDraft'
|
||||
import { TransactionError } from '@/graphql/model/TransactionError'
|
||||
import { logger } from '@/server/logger'
|
||||
import { getDataSource } from '@/typeorm/DataSource'
|
||||
|
||||
import { CreateTransactionRecipeContext } from '../transaction/CreateTransationRecipe.context'
|
||||
|
||||
import { CommunityRole } from './Community.role'
|
||||
|
||||
export class HomeCommunityRole extends CommunityRole {
|
||||
private transactionRecipe: Transaction
|
||||
|
||||
public async create(communityDraft: CommunityDraft, topic: string): Promise<void> {
|
||||
super.create(communityDraft, topic)
|
||||
// generate key pair for signing transactions and deriving all keys for community
|
||||
const keyPair = new KeyPair(new Mnemonic(CONFIG.IOTA_HOME_COMMUNITY_SEED ?? undefined))
|
||||
keyPair.fillInCommunityKeys(this.self)
|
||||
|
||||
// create auf account and gmw account
|
||||
this.self.aufAccount = AccountFactory.createAufAccount(keyPair, this.self.createdAt)
|
||||
this.self.gmwAccount = AccountFactory.createGmwAccount(keyPair, this.self.createdAt)
|
||||
|
||||
const transactionRecipeContext = new CreateTransactionRecipeContext(communityDraft, this.self)
|
||||
await transactionRecipeContext.run()
|
||||
this.transactionRecipe = transactionRecipeContext.getTransactionRecipe()
|
||||
}
|
||||
|
||||
public async store(): Promise<Community> {
|
||||
try {
|
||||
return await getDataSource().transaction(async (transactionalEntityManager) => {
|
||||
const community = await transactionalEntityManager.save(this.self)
|
||||
await transactionalEntityManager.save(this.transactionRecipe)
|
||||
return community
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('error saving home community into db: %s', error)
|
||||
throw new TransactionError(
|
||||
TransactionErrorType.DB_ERROR,
|
||||
'error saving home community into db',
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,62 @@
|
||||
import { CrossGroupType } from '@/data/proto/3_3/enum/CrossGroupType'
|
||||
import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType'
|
||||
import { TransactionDraft } from '@/graphql/input/TransactionDraft'
|
||||
import { UserIdentifier } from '@/graphql/input/UserIdentifier'
|
||||
import { TransactionError } from '@/graphql/model/TransactionError'
|
||||
|
||||
export abstract class AbstractTransactionRole {
|
||||
// eslint-disable-next-line no-useless-constructor
|
||||
public constructor(protected self: TransactionDraft) {}
|
||||
|
||||
abstract getSigningUser(): UserIdentifier
|
||||
abstract getRecipientUser(): UserIdentifier
|
||||
abstract getCrossGroupType(): CrossGroupType
|
||||
|
||||
public isCrossGroupTransaction(): boolean {
|
||||
return (
|
||||
this.self.user.communityUuid !== this.self.linkedUser.communityUuid &&
|
||||
this.self.linkedUser.communityUuid !== ''
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* otherGroup is the group/community on which this part of the transaction isn't stored
|
||||
* Alice from 'gdd1' Send 10 GDD to Bob in 'gdd2'
|
||||
* OUTBOUND came from sender, stored on sender community blockchain
|
||||
* OUTBOUND: stored on 'gdd1', otherGroup: 'gdd2'
|
||||
* INBOUND: goes to receiver, stored on receiver community blockchain
|
||||
* INBOUND: stored on 'gdd2', otherGroup: 'gdd1'
|
||||
* @returns
|
||||
*/
|
||||
public getOtherGroup(): string {
|
||||
let user: UserIdentifier
|
||||
const type = this.getCrossGroupType()
|
||||
switch (type) {
|
||||
case CrossGroupType.LOCAL:
|
||||
return ''
|
||||
case CrossGroupType.INBOUND:
|
||||
user = this.getSigningUser()
|
||||
if (!user.communityUuid) {
|
||||
throw new TransactionError(
|
||||
TransactionErrorType.MISSING_PARAMETER,
|
||||
'missing sender/signing user community id for cross group transaction',
|
||||
)
|
||||
}
|
||||
return user.communityUuid
|
||||
case CrossGroupType.OUTBOUND:
|
||||
user = this.getRecipientUser()
|
||||
if (!user.communityUuid) {
|
||||
throw new TransactionError(
|
||||
TransactionErrorType.MISSING_PARAMETER,
|
||||
'missing recipient user community id for cross group transaction',
|
||||
)
|
||||
}
|
||||
return user.communityUuid
|
||||
default:
|
||||
throw new TransactionError(
|
||||
TransactionErrorType.NOT_IMPLEMENTED_YET,
|
||||
`type not implemented yet ${type}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,25 @@
|
||||
import { Community } from '@entity/Community'
|
||||
|
||||
import { KeyPair } from '@/data/KeyPair'
|
||||
import { TransactionBodyBuilder } from '@/data/proto/TransactionBody.builder'
|
||||
import { CommunityDraft } from '@/graphql/input/CommunityDraft'
|
||||
|
||||
import { TransactionRecipeRole } from './TransactionRecipe.role'
|
||||
|
||||
export class CommunityRootTransactionRole extends TransactionRecipeRole {
|
||||
public createFromCommunityRoot(
|
||||
communityDraft: CommunityDraft,
|
||||
community: Community,
|
||||
): CommunityRootTransactionRole {
|
||||
// create proto transaction body
|
||||
const transactionBody = new TransactionBodyBuilder()
|
||||
.fromCommunityDraft(communityDraft, community)
|
||||
.build()
|
||||
// build transaction entity
|
||||
this.transactionBuilder.fromTransactionBody(transactionBody).setCommunity(community)
|
||||
const transaction = this.transactionBuilder.getTransaction()
|
||||
// sign
|
||||
this.transactionBuilder.setSignature(new KeyPair(community).sign(transaction.bodyBytes))
|
||||
return this
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,67 @@
|
||||
import { Community } from '@entity/Community'
|
||||
import { Transaction } from '@entity/Transaction'
|
||||
|
||||
import { InputTransactionType } from '@/graphql/enum/InputTransactionType'
|
||||
import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType'
|
||||
import { CommunityDraft } from '@/graphql/input/CommunityDraft'
|
||||
import { TransactionDraft } from '@/graphql/input/TransactionDraft'
|
||||
import { TransactionError } from '@/graphql/model/TransactionError'
|
||||
|
||||
import { AbstractTransactionRole } from './AbstractTransaction.role'
|
||||
import { CommunityRootTransactionRole } from './CommunityRootTransaction.role'
|
||||
import { CreationTransactionRole } from './CreationTransaction.role'
|
||||
import { ReceiveTransactionRole } from './ReceiveTransaction.role'
|
||||
import { SendTransactionRole } from './SendTransaction.role'
|
||||
import { TransactionRecipeRole } from './TransactionRecipe.role'
|
||||
|
||||
/**
|
||||
* @DCI-Context
|
||||
* Context for create and add Transaction Recipe to DB
|
||||
*/
|
||||
|
||||
export class CreateTransactionRecipeContext {
|
||||
private transactionRecipeRole: TransactionRecipeRole
|
||||
// eslint-disable-next-line no-useless-constructor
|
||||
public constructor(
|
||||
private draft: CommunityDraft | TransactionDraft,
|
||||
private community?: Community,
|
||||
) {}
|
||||
|
||||
public getTransactionRecipe(): Transaction {
|
||||
return this.transactionRecipeRole.getTransaction()
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns true if a transaction recipe was created and false if it wasn't necessary
|
||||
*/
|
||||
public async run(): Promise<boolean> {
|
||||
if (this.draft instanceof TransactionDraft) {
|
||||
this.transactionRecipeRole = new TransactionRecipeRole()
|
||||
// contain logic for translation from backend to dlt-connector format
|
||||
let transactionTypeRole: AbstractTransactionRole
|
||||
switch (this.draft.type) {
|
||||
case InputTransactionType.CREATION:
|
||||
transactionTypeRole = new CreationTransactionRole(this.draft)
|
||||
break
|
||||
case InputTransactionType.SEND:
|
||||
transactionTypeRole = new SendTransactionRole(this.draft)
|
||||
break
|
||||
case InputTransactionType.RECEIVE:
|
||||
transactionTypeRole = new ReceiveTransactionRole(this.draft)
|
||||
break
|
||||
}
|
||||
await this.transactionRecipeRole.create(this.draft, transactionTypeRole)
|
||||
return true
|
||||
} else if (this.draft instanceof CommunityDraft) {
|
||||
if (!this.community) {
|
||||
throw new TransactionError(TransactionErrorType.MISSING_PARAMETER, 'community was not set')
|
||||
}
|
||||
this.transactionRecipeRole = new CommunityRootTransactionRole().createFromCommunityRoot(
|
||||
this.draft,
|
||||
this.community,
|
||||
)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
import { CrossGroupType } from '@/data/proto/3_3/enum/CrossGroupType'
|
||||
import { UserIdentifier } from '@/graphql/input/UserIdentifier'
|
||||
|
||||
import { AbstractTransactionRole } from './AbstractTransaction.role'
|
||||
|
||||
export class CreationTransactionRole extends AbstractTransactionRole {
|
||||
public getSigningUser(): UserIdentifier {
|
||||
return this.self.linkedUser
|
||||
}
|
||||
|
||||
public getRecipientUser(): UserIdentifier {
|
||||
return this.self.user
|
||||
}
|
||||
|
||||
public getCrossGroupType(): CrossGroupType {
|
||||
return CrossGroupType.LOCAL
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
import { CrossGroupType } from '@/data/proto/3_3/enum/CrossGroupType'
|
||||
import { UserIdentifier } from '@/graphql/input/UserIdentifier'
|
||||
|
||||
import { AbstractTransactionRole } from './AbstractTransaction.role'
|
||||
|
||||
export class ReceiveTransactionRole extends AbstractTransactionRole {
|
||||
public getSigningUser(): UserIdentifier {
|
||||
return this.self.linkedUser
|
||||
}
|
||||
|
||||
public getRecipientUser(): UserIdentifier {
|
||||
return this.self.user
|
||||
}
|
||||
|
||||
public getCrossGroupType(): CrossGroupType {
|
||||
if (this.isCrossGroupTransaction()) {
|
||||
return CrossGroupType.INBOUND
|
||||
}
|
||||
return CrossGroupType.LOCAL
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
import { CrossGroupType } from '@/data/proto/3_3/enum/CrossGroupType'
|
||||
import { UserIdentifier } from '@/graphql/input/UserIdentifier'
|
||||
|
||||
import { AbstractTransactionRole } from './AbstractTransaction.role'
|
||||
|
||||
export class SendTransactionRole extends AbstractTransactionRole {
|
||||
public getSigningUser(): UserIdentifier {
|
||||
return this.self.user
|
||||
}
|
||||
|
||||
public getRecipientUser(): UserIdentifier {
|
||||
return this.self.linkedUser
|
||||
}
|
||||
|
||||
public getCrossGroupType(): CrossGroupType {
|
||||
if (this.isCrossGroupTransaction()) {
|
||||
return CrossGroupType.OUTBOUND
|
||||
}
|
||||
return CrossGroupType.LOCAL
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,70 @@
|
||||
import { Transaction } from '@entity/Transaction'
|
||||
|
||||
import { KeyPair } from '@/data/KeyPair'
|
||||
import { TransactionBodyBuilder } from '@/data/proto/TransactionBody.builder'
|
||||
import { TransactionBuilder } from '@/data/Transaction.builder'
|
||||
import { UserRepository } from '@/data/User.repository'
|
||||
import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType'
|
||||
import { TransactionDraft } from '@/graphql/input/TransactionDraft'
|
||||
import { TransactionError } from '@/graphql/model/TransactionError'
|
||||
|
||||
import { AbstractTransactionRole } from './AbstractTransaction.role'
|
||||
|
||||
export class TransactionRecipeRole {
|
||||
protected transactionBuilder: TransactionBuilder
|
||||
|
||||
public constructor() {
|
||||
this.transactionBuilder = new TransactionBuilder()
|
||||
}
|
||||
|
||||
public async create(
|
||||
transactionDraft: TransactionDraft,
|
||||
transactionTypeRole: AbstractTransactionRole,
|
||||
): Promise<TransactionRecipeRole> {
|
||||
const signingUser = transactionTypeRole.getSigningUser()
|
||||
const recipientUser = transactionTypeRole.getRecipientUser()
|
||||
|
||||
// loading signing and recipient account
|
||||
// TODO: look for ways to use only one db call for both
|
||||
const signingAccount = await UserRepository.findAccountByUserIdentifier(signingUser)
|
||||
if (!signingAccount) {
|
||||
throw new TransactionError(
|
||||
TransactionErrorType.NOT_FOUND,
|
||||
"couldn't found sender user account in db",
|
||||
)
|
||||
}
|
||||
const recipientAccount = await UserRepository.findAccountByUserIdentifier(recipientUser)
|
||||
if (!recipientAccount) {
|
||||
throw new TransactionError(
|
||||
TransactionErrorType.NOT_FOUND,
|
||||
"couldn't found recipient user account in db",
|
||||
)
|
||||
}
|
||||
// create proto transaction body
|
||||
const transactionBodyBuilder = new TransactionBodyBuilder()
|
||||
.setSigningAccount(signingAccount)
|
||||
.setRecipientAccount(recipientAccount)
|
||||
.fromTransactionDraft(transactionDraft)
|
||||
.setCrossGroupType(transactionTypeRole.getCrossGroupType())
|
||||
.setOtherGroup(transactionTypeRole.getOtherGroup())
|
||||
|
||||
// build transaction entity
|
||||
this.transactionBuilder
|
||||
.fromTransactionBodyBuilder(transactionBodyBuilder)
|
||||
.addBackendTransaction(transactionDraft)
|
||||
await this.transactionBuilder.setSenderCommunityFromSenderUser(signingUser)
|
||||
if (recipientUser.communityUuid !== signingUser.communityUuid) {
|
||||
await this.transactionBuilder.setOtherCommunityFromRecipientUser(recipientUser)
|
||||
}
|
||||
const transaction = this.transactionBuilder.getTransaction()
|
||||
// sign
|
||||
this.transactionBuilder.setSignature(
|
||||
new KeyPair(this.transactionBuilder.getCommunity()).sign(transaction.bodyBytes),
|
||||
)
|
||||
return this
|
||||
}
|
||||
|
||||
public getTransaction(): Transaction {
|
||||
return this.transactionBuilder.getTransaction()
|
||||
}
|
||||
}
|
||||
@ -1,32 +0,0 @@
|
||||
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
|
||||
}
|
||||
@ -1,21 +0,0 @@
|
||||
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
|
||||
}
|
||||
@ -1,23 +0,0 @@
|
||||
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
|
||||
}
|
||||
@ -1,19 +0,0 @@
|
||||
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
|
||||
}
|
||||
@ -1,66 +0,0 @@
|
||||
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
|
||||
}
|
||||
@ -2,15 +2,16 @@ import 'reflect-metadata'
|
||||
|
||||
import { ApolloServer } from '@apollo/server'
|
||||
import { expressMiddleware } from '@apollo/server/express4'
|
||||
import bodyParser from 'body-parser'
|
||||
import cors from 'cors'
|
||||
import express, { Express } from 'express'
|
||||
|
||||
// graphql
|
||||
import { Logger } from 'log4js'
|
||||
|
||||
import { schema } from '@/graphql/schema'
|
||||
import { Connection } from '@/typeorm/DataSource'
|
||||
|
||||
import { logger as dltLogger } from './logger'
|
||||
import { Logger } from 'log4js'
|
||||
import cors from 'cors'
|
||||
import bodyParser from 'body-parser'
|
||||
|
||||
type ServerDef = { apollo: ApolloServer; app: Express }
|
||||
|
||||
@ -27,6 +28,8 @@ const createServer = async (
|
||||
logger.addContext('user', 'unknown')
|
||||
logger.debug('createServer...')
|
||||
|
||||
// connect to db and test db version
|
||||
await Connection.getInstance().init()
|
||||
// Express Server
|
||||
const app = express()
|
||||
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
import { readFileSync } from 'fs'
|
||||
|
||||
import log4js from 'log4js'
|
||||
|
||||
import { CONFIG } from '@/config'
|
||||
|
||||
import { readFileSync } from 'fs'
|
||||
const options = JSON.parse(readFileSync(CONFIG.LOG4JS_CONFIG, 'utf-8'))
|
||||
|
||||
log4js.configure(options)
|
||||
|
||||
@ -1,28 +0,0 @@
|
||||
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 }
|
||||
@ -2,25 +2,88 @@
|
||||
// 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 { Migration } from '@entity/Migration'
|
||||
|
||||
import { CONFIG } from '@/config'
|
||||
import { LogError } from '@/server/LogError'
|
||||
import { logger } from '@/server/logger'
|
||||
|
||||
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',
|
||||
},
|
||||
})
|
||||
// eslint-disable-next-line @typescript-eslint/no-extraneous-class
|
||||
export class Connection {
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
private static instance: Connection
|
||||
private connection: DBDataSource
|
||||
|
||||
export const getDataSource = () => DataSource
|
||||
/**
|
||||
* The Singleton's constructor should always be private to prevent direct
|
||||
* construction calls with the `new` operator.
|
||||
*/
|
||||
// eslint-disable-next-line no-useless-constructor, @typescript-eslint/no-empty-function
|
||||
private constructor() {
|
||||
this.connection = 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',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* The static method that controls the access to the singleton instance.
|
||||
*
|
||||
* This implementation let you subclass the Singleton class while keeping
|
||||
* just one instance of each subclass around.
|
||||
*/
|
||||
public static getInstance(): Connection {
|
||||
if (!Connection.instance) {
|
||||
Connection.instance = new Connection()
|
||||
}
|
||||
return Connection.instance
|
||||
}
|
||||
|
||||
public getDataSource(): DBDataSource {
|
||||
return this.connection
|
||||
}
|
||||
|
||||
public async init(): Promise<void> {
|
||||
await this.connection.initialize()
|
||||
try {
|
||||
Connection.getInstance()
|
||||
} catch (error) {
|
||||
// try and catch for logging
|
||||
logger.fatal(`Couldn't open connection to database!`)
|
||||
throw error
|
||||
}
|
||||
|
||||
// check for correct database version
|
||||
await this.checkDBVersion(CONFIG.DB_VERSION)
|
||||
}
|
||||
|
||||
async checkDBVersion(DB_VERSION: string): Promise<void> {
|
||||
const dbVersion = await Migration.find({ order: { version: 'DESC' }, take: 1 })
|
||||
if (!dbVersion || dbVersion.length < 1) {
|
||||
throw new LogError('found no db version in migrations, could dlt-database run successfully?')
|
||||
}
|
||||
// return dbVersion ? dbVersion.fileName : null
|
||||
if (!dbVersion[0].fileName.includes(DB_VERSION)) {
|
||||
throw new LogError(
|
||||
`Wrong database version detected - the backend requires '${DB_VERSION}' but found '${
|
||||
dbVersion[0].fileName ?? 'None'
|
||||
}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const getDataSource = () => Connection.getInstance().getDataSource()
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user