From 2ff7adef52312a1086c78596647008fc7843fe4d Mon Sep 17 00:00:00 2001 From: einhorn_b Date: Mon, 23 Oct 2023 19:07:13 +0200 Subject: [PATCH] first draft --- dlt-connector/@types/bip32-ed25519/index.d.ts | 1 + dlt-connector/package.json | 4 +- .../src/controller/Community.test.ts | 66 ------- dlt-connector/src/controller/Community.ts | 28 --- .../src/controller/GradidoTransaction.ts | 10 -- .../src/controller/KeyManager.test.ts | 22 +++ dlt-connector/src/controller/KeyManager.ts | 123 +++++++++++++ .../src/controller/TransactionBody.test.ts | 162 ------------------ dlt-connector/src/data/Account.repository.ts | 33 ++++ .../src/data/Community.repository.ts | 74 ++++++++ dlt-connector/src/data/KeyPair.ts | 38 ++++ dlt-connector/src/data/Transaction.builder.ts | 147 ++++++++++++++++ .../src/data/Transaction.repository.ts | 48 ++++++ dlt-connector/src/data/account.factory.ts | 42 +++++ dlt-connector/src/data/community.factory.ts | 31 ++++ .../src/data/proto/3_3/CommunityRoot.ts | 40 +++++ .../proto/3_3/ConfirmedTransaction.ts} | 12 +- .../proto/3_3/GradidoCreation.test.ts | 0 .../src/data/proto/3_3/GradidoCreation.ts | 53 ++++++ .../proto/3_3/GradidoDeferredTransfer.ts | 22 ++- .../src/data/proto/3_3/GradidoTransaction.ts | 43 +++++ .../src/data/proto/3_3/GradidoTransfer.ts | 48 ++++++ .../proto/3_3/GroupFriendsUpdate.ts | 16 +- .../src/data/proto/3_3/RegisterAddress.ts | 33 ++++ .../src/{ => data}/proto/3_3/SignatureMap.ts | 8 +- .../src/{ => data}/proto/3_3/SignaturePair.ts | 6 +- .../{ => data}/proto/3_3/Timestamp.test.ts | 0 .../src/{ => data}/proto/3_3/Timestamp.ts | 2 +- .../proto/3_3/TimestampSeconds.test.ts | 0 .../{ => data}/proto/3_3/TimestampSeconds.ts | 2 +- .../src/data/proto/3_3/TransactionBody.ts | 129 ++++++++++++++ .../{ => data}/proto/3_3/TransferAmount.ts | 2 +- .../src/data/proto/3_3/enum/AddressType.ts | 19 ++ .../src/data/proto/3_3/enum/CrossGroupType.ts | 7 + .../proto}/TransactionBase.ts | 3 + .../src/data/proto/TransactionBody.builder.ts | 95 ++++++++++ .../proto/transactionBody.logic.ts} | 32 +--- dlt-connector/src/graphql/arg/CommunityArg.ts | 19 ++ .../src/graphql/enum/InputTransactionType.ts | 12 ++ .../src/graphql/enum/TransactionErrorType.ts | 2 + .../src/graphql/enum/TransactionType.ts | 27 +-- .../src/graphql/input/TransactionDraft.ts | 8 +- dlt-connector/src/graphql/model/Community.ts | 36 ++++ .../src/graphql/model/TransactionRecipe.ts | 25 +++ .../src/graphql/model/TransactionResult.ts | 13 +- .../src/graphql/resolver/CommunityResolver.ts | 82 +++++---- .../graphql/resolver/TransactionsResolver.ts | 74 ++++++-- .../backendToDb/community/Community.role.ts | 6 + .../community/ForeignCommunity.role.ts | 19 ++ .../community/HomeCommunity.role.ts | 28 +++ .../community/community.context.ts | 30 ++++ .../CommunityRootTransaction.role.ts | 24 +++ .../transaction/TransactionRecipe.role.ts | 49 ++++++ .../transaction/transaction.context.ts | 20 +++ .../src/proto/3_3/GradidoCreation.ts | 32 ---- .../src/proto/3_3/GradidoTransaction.ts | 21 --- .../src/proto/3_3/GradidoTransfer.ts | 23 --- .../src/proto/3_3/RegisterAddress.ts | 19 -- .../src/proto/3_3/TransactionBody.ts | 66 ------- dlt-connector/src/utils/cryptoHelper.ts | 6 + .../src/utils/derivationHelper.test.ts | 15 ++ dlt-connector/src/utils/derivationHelper.ts | 17 ++ dlt-connector/src/utils/typeConverter.test.ts | 12 ++ dlt-connector/src/utils/typeConverter.ts | 45 +++++ dlt-connector/tsconfig.json | 3 +- dlt-connector/yarn.lock | 113 +++++++++++- dlt-database/entity/0001-init_db/Account.ts | 2 +- dlt-database/entity/0001-init_db/Community.ts | 2 +- .../0001-init_db/ConfirmedTransaction.ts | 2 +- .../entity/0001-init_db/TransactionRecipe.ts | 4 +- .../ConfirmedTransaction.ts | 2 +- .../Account.ts | 92 ++++++++++ .../Community.ts | 68 ++++++++ .../Transaction.ts | 120 +++++++++++++ dlt-database/entity/Account.ts | 2 +- dlt-database/entity/Community.ts | 2 +- dlt-database/entity/Transaction.ts | 1 + dlt-database/entity/index.ts | 6 +- .../0003-refactor_transaction_recipe.ts | 97 +++++++++++ 79 files changed, 2092 insertions(+), 555 deletions(-) create mode 100644 dlt-connector/@types/bip32-ed25519/index.d.ts delete mode 100644 dlt-connector/src/controller/Community.test.ts delete mode 100644 dlt-connector/src/controller/Community.ts delete mode 100644 dlt-connector/src/controller/GradidoTransaction.ts create mode 100644 dlt-connector/src/controller/KeyManager.test.ts create mode 100644 dlt-connector/src/controller/KeyManager.ts delete mode 100644 dlt-connector/src/controller/TransactionBody.test.ts create mode 100644 dlt-connector/src/data/Account.repository.ts create mode 100644 dlt-connector/src/data/Community.repository.ts create mode 100644 dlt-connector/src/data/KeyPair.ts create mode 100644 dlt-connector/src/data/Transaction.builder.ts create mode 100644 dlt-connector/src/data/Transaction.repository.ts create mode 100644 dlt-connector/src/data/account.factory.ts create mode 100644 dlt-connector/src/data/community.factory.ts create mode 100644 dlt-connector/src/data/proto/3_3/CommunityRoot.ts rename dlt-connector/src/{proto/3_3/GradidoConfirmedTransaction.ts => data/proto/3_3/ConfirmedTransaction.ts} (67%) rename dlt-connector/src/{ => data}/proto/3_3/GradidoCreation.test.ts (100%) create mode 100644 dlt-connector/src/data/proto/3_3/GradidoCreation.ts rename dlt-connector/src/{ => data}/proto/3_3/GradidoDeferredTransfer.ts (65%) create mode 100644 dlt-connector/src/data/proto/3_3/GradidoTransaction.ts create mode 100644 dlt-connector/src/data/proto/3_3/GradidoTransfer.ts rename dlt-connector/src/{ => data}/proto/3_3/GroupFriendsUpdate.ts (52%) create mode 100644 dlt-connector/src/data/proto/3_3/RegisterAddress.ts rename dlt-connector/src/{ => data}/proto/3_3/SignatureMap.ts (66%) rename dlt-connector/src/{ => data}/proto/3_3/SignaturePair.ts (63%) rename dlt-connector/src/{ => data}/proto/3_3/Timestamp.test.ts (100%) rename dlt-connector/src/{ => data}/proto/3_3/Timestamp.ts (94%) rename dlt-connector/src/{ => data}/proto/3_3/TimestampSeconds.test.ts (100%) rename dlt-connector/src/{ => data}/proto/3_3/TimestampSeconds.ts (91%) create mode 100644 dlt-connector/src/data/proto/3_3/TransactionBody.ts rename dlt-connector/src/{ => data}/proto/3_3/TransferAmount.ts (88%) create mode 100644 dlt-connector/src/data/proto/3_3/enum/AddressType.ts create mode 100644 dlt-connector/src/data/proto/3_3/enum/CrossGroupType.ts rename dlt-connector/src/{controller => data/proto}/TransactionBase.ts (73%) create mode 100644 dlt-connector/src/data/proto/TransactionBody.builder.ts rename dlt-connector/src/{controller/TransactionBody.ts => data/proto/transactionBody.logic.ts} (60%) create mode 100644 dlt-connector/src/graphql/arg/CommunityArg.ts create mode 100755 dlt-connector/src/graphql/enum/InputTransactionType.ts mode change 100755 => 100644 dlt-connector/src/graphql/enum/TransactionType.ts create mode 100644 dlt-connector/src/graphql/model/Community.ts create mode 100644 dlt-connector/src/graphql/model/TransactionRecipe.ts create mode 100644 dlt-connector/src/interactions/backendToDb/community/Community.role.ts create mode 100644 dlt-connector/src/interactions/backendToDb/community/ForeignCommunity.role.ts create mode 100644 dlt-connector/src/interactions/backendToDb/community/HomeCommunity.role.ts create mode 100644 dlt-connector/src/interactions/backendToDb/community/community.context.ts create mode 100644 dlt-connector/src/interactions/backendToDb/transaction/CommunityRootTransaction.role.ts create mode 100644 dlt-connector/src/interactions/backendToDb/transaction/TransactionRecipe.role.ts create mode 100644 dlt-connector/src/interactions/backendToDb/transaction/transaction.context.ts delete mode 100644 dlt-connector/src/proto/3_3/GradidoCreation.ts delete mode 100644 dlt-connector/src/proto/3_3/GradidoTransaction.ts delete mode 100644 dlt-connector/src/proto/3_3/GradidoTransfer.ts delete mode 100644 dlt-connector/src/proto/3_3/RegisterAddress.ts delete mode 100644 dlt-connector/src/proto/3_3/TransactionBody.ts create mode 100644 dlt-connector/src/utils/cryptoHelper.ts create mode 100644 dlt-connector/src/utils/derivationHelper.test.ts create mode 100644 dlt-connector/src/utils/derivationHelper.ts create mode 100644 dlt-connector/src/utils/typeConverter.test.ts create mode 100644 dlt-database/entity/0003-refactor_transaction_recipe/Account.ts create mode 100644 dlt-database/entity/0003-refactor_transaction_recipe/Community.ts create mode 100644 dlt-database/entity/0003-refactor_transaction_recipe/Transaction.ts create mode 100644 dlt-database/entity/Transaction.ts create mode 100644 dlt-database/migrations/0003-refactor_transaction_recipe.ts diff --git a/dlt-connector/@types/bip32-ed25519/index.d.ts b/dlt-connector/@types/bip32-ed25519/index.d.ts new file mode 100644 index 000000000..7a3375ab6 --- /dev/null +++ b/dlt-connector/@types/bip32-ed25519/index.d.ts @@ -0,0 +1 @@ +declare module 'bip32-ed25519' diff --git a/dlt-connector/package.json b/dlt-connector/package.json index 47e9136ff..d64b6d743 100644 --- a/dlt-connector/package.json +++ b/dlt-connector/package.json @@ -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", diff --git a/dlt-connector/src/controller/Community.test.ts b/dlt-connector/src/controller/Community.test.ts deleted file mode 100644 index d8c5ad0de..000000000 --- a/dlt-connector/src/controller/Community.test.ts +++ /dev/null @@ -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'), - }) - }) - }) -}) diff --git a/dlt-connector/src/controller/Community.ts b/dlt-connector/src/controller/Community.ts deleted file mode 100644 index eff1b2b64..000000000 --- a/dlt-connector/src/controller/Community.ts +++ /dev/null @@ -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 => { - 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 => { - const communities = await Community.find({ select: { iotaTopic: true } }) - return communities.map((community) => community.iotaTopic) -} diff --git a/dlt-connector/src/controller/GradidoTransaction.ts b/dlt-connector/src/controller/GradidoTransaction.ts deleted file mode 100644 index 671f3f57a..000000000 --- a/dlt-connector/src/controller/GradidoTransaction.ts +++ /dev/null @@ -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 -} diff --git a/dlt-connector/src/controller/KeyManager.test.ts b/dlt-connector/src/controller/KeyManager.test.ts new file mode 100644 index 000000000..7f3d26932 --- /dev/null +++ b/dlt-connector/src/controller/KeyManager.test.ts @@ -0,0 +1,22 @@ +/* eslint-disable camelcase */ +import { entropyToMnemonic, mnemonicToSeedSync } from 'bip39' +import { generateFromSeed, toPublic } from 'bip32-ed25519' + +describe('controller/KeyManager', () => { + describe('test crypto lib', () => { + it('key length', () => { + const mnemonic = entropyToMnemonic( + 'aabbccddeeff00112233445566778899aabbccddeeff00112233445566778899', + ) + expect(mnemonic).toEqual( + 'primary taxi danger target useless ancient match hammer fever crisp timber crew produce toy jeans that abandon math mimic master filter design carbon carbon', + ) + const seed = mnemonicToSeedSync(mnemonic) + // private key 64 Bytes + 32 Byte ChainCode + const extendPrivkey = generateFromSeed(seed) + expect(extendPrivkey).toHaveLength(96) + // public key 32 Bytes + 32 Bytes ChainCode + expect(toPublic(extendPrivkey)).toHaveLength(64) + }) + }) +}) diff --git a/dlt-connector/src/controller/KeyManager.ts b/dlt-connector/src/controller/KeyManager.ts new file mode 100644 index 000000000..df1e61b33 --- /dev/null +++ b/dlt-connector/src/controller/KeyManager.ts @@ -0,0 +1,123 @@ +// eslint-disable-next-line camelcase +import { randombytes_buf } from 'sodium-native' +import { CONFIG } from '../config' +import { entropyToMnemonic, mnemonicToSeedSync } from 'bip39' +// https://www.npmjs.com/package/bip32-ed25519?activeTab=code +import { + generateFromSeed, + derivePrivate, + sign as ed25519Sign, + verify as ed25519Verify, +} from 'bip32-ed25519' +import { logger } from '@/server/logger' +import { LogError } from '@/server/LogError' +import { KeyPair } from '@/data/KeyPair' +import { GradidoTransaction } from '@/data/proto/3_3/GradidoTransaction' +import { CommunityRepository } from '@/data/Community.repository' +import { SignaturePair } from '@/data/proto/3_3/SignaturePair' + +// Source: https://refactoring.guru/design-patterns/singleton/typescript/example +// and ../federation/client/FederationClientFactory.ts +/** + * A Singleton class defines the `getInstance` method that lets clients access + * the unique singleton instance. + */ +// eslint-disable-next-line @typescript-eslint/no-extraneous-class +export class KeyManager { + // eslint-disable-next-line no-use-before-define + private static instance: KeyManager + private homeCommunityRootKeys: KeyPair | null = null + + /** + * 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() {} + + /** + * 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(): KeyManager { + if (!KeyManager.instance) { + KeyManager.instance = new KeyManager() + } + return KeyManager.instance + } + + public async init(): Promise { + try { + this.homeCommunityRootKeys = await CommunityRepository.loadHomeCommunityKeyPair() + return true + } catch (error) { + logger.error('error by init key manager', error) + return false + } + } + + public static generateKeyPair(): KeyPair { + const mnemonic = KeyManager.generateMnemonic(CONFIG.IOTA_HOME_COMMUNITY_SEED ?? undefined) + // logger.info('passphrase for key pair: ' + mnemonic) + const seed = mnemonicToSeedSync(mnemonic) + return new KeyPair(generateFromSeed(seed)) + } + + public setHomeCommunityKeyPair(keyPair: KeyPair) { + this.homeCommunityRootKeys = keyPair + } + + public sign(transaction: GradidoTransaction, keys?: KeyPair[]) { + let localKeys: KeyPair[] = [] + + if (!keys && this.homeCommunityRootKeys) { + localKeys.push(this.homeCommunityRootKeys) + } else if (keys) { + localKeys = keys + } + if (!localKeys.length) { + throw new LogError('no key pair for signing') + } + localKeys.forEach((keyPair: KeyPair) => { + const signature = ed25519Sign(transaction.bodyBytes, keyPair.getExtendPrivateKey()) + const sigPair = new SignaturePair({ pubKey: keyPair.publicKey, signature }) + logger.debug('sign transaction', { + signature: signature.toString('hex'), + publicKey: keyPair.publicKey.toString('hex'), + bodyBytes: transaction.bodyBytes.toString('hex'), + }) + transaction.sigMap.sigPair.push(sigPair) + }) + } + + public getHomeCommunityPublicKey(): Buffer | undefined { + if (!this.homeCommunityRootKeys) return undefined + return this.homeCommunityRootKeys.publicKey + } + + public derive(path: number[], parentKeys?: KeyPair): KeyPair { + const extendedPrivateKey = parentKeys + ? parentKeys.getExtendPrivateKey() + : this.homeCommunityRootKeys?.getExtendPrivateKey() + if (!extendedPrivateKey) { + throw new LogError('missing parent or root key pair') + } + return new KeyPair( + path.reduce( + (extendPrivateKey: Buffer, node: number) => derivePrivate(extendPrivateKey, node), + extendedPrivateKey, + ), + ) + } + + static generateMnemonic(seed?: Buffer | string): string { + if (seed) { + return entropyToMnemonic(seed) + } + const entropy = Buffer.alloc(256) + randombytes_buf(entropy) + return entropyToMnemonic(entropy) + } +} diff --git a/dlt-connector/src/controller/TransactionBody.test.ts b/dlt-connector/src/controller/TransactionBody.test.ts deleted file mode 100644 index eac613ab7..000000000 --- a/dlt-connector/src/controller/TransactionBody.test.ts +++ /dev/null @@ -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', - ) - }) - }) -}) diff --git a/dlt-connector/src/data/Account.repository.ts b/dlt-connector/src/data/Account.repository.ts new file mode 100644 index 000000000..2a228c261 --- /dev/null +++ b/dlt-connector/src/data/Account.repository.ts @@ -0,0 +1,33 @@ +import { UserIdentifier } from '@/graphql/input/UserIdentifier' +import { getDataSource } from '@/typeorm/DataSource' +import { Account } from '@entity/Account' +import { User } from '@entity/User' +import { In } from 'typeorm' + +export const AccountRepository = getDataSource() + .getRepository(Account) + .extend({ + findAccountsByPublicKeys(publicKeys: Buffer[]): Promise { + return Account.findBy({ derive2Pubkey: In(publicKeys) }) + }, + + async findAccountByPublicKey(publicKey: Buffer | undefined): Promise { + if (!publicKey) return undefined + return (await Account.findOneBy({ derive2Pubkey: Buffer.from(publicKey) })) ?? undefined + }, + + async findAccountByUserIdentifier({ + uuid, + accountNr, + }: UserIdentifier): Promise { + 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 + } + }, + }) diff --git a/dlt-connector/src/data/Community.repository.ts b/dlt-connector/src/data/Community.repository.ts new file mode 100644 index 000000000..1624404d3 --- /dev/null +++ b/dlt-connector/src/data/Community.repository.ts @@ -0,0 +1,74 @@ +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 { getDataSource } from '@/typeorm/DataSource' +import { iotaTopicFromCommunityUUID } from '@/utils/typeConverter' +import { Community } from '@entity/Community' +import { FindOptionsSelect, In, IsNull, Not } from 'typeorm' +import { KeyPair } from './KeyPair' +import { LogError } from '@/server/LogError' + +export const CommunityRepository = getDataSource() + .getRepository(Community) + .extend({ + async isExist(community: CommunityDraft | string): Promise { + const iotaTopic = + community instanceof CommunityDraft ? iotaTopicFromCommunityUUID(community.uuid) : community + const result = await Community.find({ + where: { iotaTopic }, + }) + return result.length > 0 + }, + + async findByCommunityArg({ uuid, foreign, confirmed }: CommunityArg): Promise { + return await Community.find({ + where: { + ...(uuid && { iotaTopic: iotaTopicFromCommunityUUID(uuid) }), + ...(foreign && { foreign }), + ...(confirmed && { confirmedAt: Not(IsNull()) }), + }, + }) + }, + + async findByCommunityUuid(communityUuid: string): Promise { + return await Community.findOneBy({ iotaTopic: iotaTopicFromCommunityUUID(communityUuid) }) + }, + + async findByIotaTopic(iotaTopic: string): Promise { + return await Community.findOneBy({ iotaTopic }) + }, + + findCommunitiesByTopics(topics: string[]): Promise { + return Community.findBy({ iotaTopic: In(topics) }) + }, + + async getCommunityForUserIdentifier( + identifier: UserIdentifier, + ): Promise { + if (!identifier.communityUuid) { + throw new TransactionError(TransactionErrorType.MISSING_PARAMETER, 'community uuid not set') + } + return ( + (await Community.findOneBy({ + iotaTopic: iotaTopicFromCommunityUUID(identifier.communityUuid), + })) ?? undefined + ) + }, + + findAll(select: FindOptionsSelect): Promise { + return Community.find({ select }) + }, + + async loadHomeCommunityKeyPair(): Promise { + const community = await Community.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) + }, + }) diff --git a/dlt-connector/src/data/KeyPair.ts b/dlt-connector/src/data/KeyPair.ts new file mode 100644 index 000000000..3487c1d95 --- /dev/null +++ b/dlt-connector/src/data/KeyPair.ts @@ -0,0 +1,38 @@ +// https://www.npmjs.com/package/bip32-ed25519?activeTab=code +import { toPublic } from 'bip32-ed25519' +import { Community } from '@entity/Community' +import { LogError } from '@/server/LogError' + +export class KeyPair { + /** + * @param input: Buffer = extended private key, returned from bip32-ed25519 generateFromSeed + * @param input: Community = community entity with keys loaded from db + * + */ + public constructor(input: Buffer | Community) { + if (input instanceof Buffer) { + this.privateKey = input.subarray(0, 64) + this.chainCode = input.subarray(64, 96) + this.publicKey = toPublic(input).subarray(0, 32) + } 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 + } + } + + public getExtendPrivateKey(): Buffer { + return Buffer.concat([this.privateKey, this.chainCode]) + } + + public getExtendPublicKey(): Buffer { + return Buffer.concat([this.publicKey, this.chainCode]) + } + + publicKey: Buffer + chainCode: Buffer + privateKey: Buffer +} diff --git a/dlt-connector/src/data/Transaction.builder.ts b/dlt-connector/src/data/Transaction.builder.ts new file mode 100644 index 000000000..4b1b385ab --- /dev/null +++ b/dlt-connector/src/data/Transaction.builder.ts @@ -0,0 +1,147 @@ +import { GradidoTransaction } from '@/data/proto/3_3/GradidoTransaction' +import { TransactionBody } from '@/data/proto/3_3/TransactionBody' +import { bodyBytesToTransactionBody, transactionBodyToBodyBytes } from '@/utils/typeConverter' +import { Transaction } from '@entity/Transaction' +import { AccountRepository } from './Account.repository' +import { UserIdentifier } from '@/graphql/input/UserIdentifier' +import { CommunityRepository } from './Community.repository' +import { LogError } from '@/server/LogError' +import { Account } from '@entity/Account' +import { Community } from '@entity/Community' + +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 setSigningAccount(signingAccount: Account): TransactionBuilder { + this.transaction.signingAccount = signingAccount + return this + } + + public setRecipientAccount(recipientAccount: Account): TransactionBuilder { + this.transaction.recipientAccount = recipientAccount + return this + } + + public setSenderCommunity(senderCommunity: Community): TransactionBuilder { + this.transaction.senderCommunity = senderCommunity + return this + } + + public setRecipientCommunity(recipientCommunity?: Community): TransactionBuilder { + if (!this.transaction.senderCommunity) { + throw new LogError('Please set sender community first!') + } + + this.transaction.recipientCommunity = + recipientCommunity && + this.transaction.senderCommunity && + this.transaction.senderCommunity.id !== recipientCommunity.id + ? recipientCommunity + : undefined + return this + } + + public setSignature(signature: Buffer): TransactionBuilder { + this.transaction.signature = signature + return this + } + + public async setSenderCommunityFromSenderUser( + senderUser: UserIdentifier, + ): Promise { + // get sender community + const senderCommunity = await CommunityRepository.getCommunityForUserIdentifier(senderUser) + if (!senderCommunity) { + throw new LogError("couldn't find sender community for transaction") + } + return this.setSenderCommunity(senderCommunity) + } + + public async setRecipientCommunityFromRecipientUser( + recipientUser: UserIdentifier, + ): Promise { + // get recipient community + const recipientCommunity = await CommunityRepository.getCommunityForUserIdentifier( + recipientUser, + ) + return this.setRecipientCommunity(recipientCommunity) + } + + public async fromGradidoTransactionSearchForAccounts( + gradidoTransaction: GradidoTransaction, + ): Promise { + 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 + } +} diff --git a/dlt-connector/src/data/Transaction.repository.ts b/dlt-connector/src/data/Transaction.repository.ts new file mode 100644 index 000000000..5a2a185bf --- /dev/null +++ b/dlt-connector/src/data/Transaction.repository.ts @@ -0,0 +1,48 @@ +import { getDataSource } from '@/typeorm/DataSource' +import { Transaction } from '@entity/Transaction' +import { IsNull } from 'typeorm' + +// 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 { + return this.findOneBy({ signature: Buffer.from(signature) }) + }, + findByMessageId(iotaMessageId: string): Promise { + return this.findOneBy({ iotaMessageId: Buffer.from(iotaMessageId, 'hex') }) + }, + async getNextPendingTransaction(): Promise { + return await this.findOne({ + where: { iotaMessageId: IsNull() }, + order: { createdAt: 'ASC' }, + relations: { signingAccount: true }, + }) + }, + async findExistingTransactionAndMissingMessageIds(messageIDsHex: string[]): Promise<{ + existingTransactions: Transaction[] + missingMessageIdsHex: string[] + }> { + const existingTransactions = await this.createQueryBuilder('Transaction') + .where('HEX(Transaction.iota_message_id) IN (:...messageIDs)', { + messageIDs: messageIDsHex, + }) + .leftJoinAndSelect('Transaction.recipientAccount', 'RecipientAccount') + .leftJoinAndSelect('RecipientAccount.user', 'RecipientUser') + .leftJoinAndSelect('Transaction.signingAccount', 'SigningAccount') + .leftJoinAndSelect('SigningAccount.user', 'SigningUser') + .getMany() + + const foundMessageIds = existingTransactions + .map((recipe) => recipe.iotaMessageId?.toString('hex')) + .filter((messageId) => !!messageId) + // find message ids for which we don't already have a transaction recipe + const missingMessageIdsHex = messageIDsHex.filter( + (id: string) => !foundMessageIds.includes(id), + ) + return { existingTransactions, missingMessageIdsHex } + }, + async removeConfirmedTransaction(transactions: Transaction[]): Promise { + return transactions.filter((transaction: Transaction) => transaction.runningHash.length === 0) + }, + }) diff --git a/dlt-connector/src/data/account.factory.ts b/dlt-connector/src/data/account.factory.ts new file mode 100644 index 000000000..d823c8345 --- /dev/null +++ b/dlt-connector/src/data/account.factory.ts @@ -0,0 +1,42 @@ +import { KeyManager } from '@/controller/KeyManager' +import { KeyPair } from '@/data/KeyPair' +import { AddressType } from '@/data/proto/3_3/enum/AddressType' +import { hardenDerivationIndex } from '@/utils/derivationHelper' +import { Account } from '@entity/Account' +import Decimal from 'decimal.js-light' + +const GMW_ACCOUNT_DERIVATION_INDEX = 1 +const AUF_ACCOUNT_DERIVATION_INDEX = 2 + +export const createAccount = ( + keyPair: KeyPair, + createdAt: Date, + derivationIndex: number, + type: AddressType, +): Account => { + const account = Account.create() + account.derivationIndex = derivationIndex + account.derive2Pubkey = KeyManager.getInstance().derive([derivationIndex], keyPair).publicKey + account.type = type.valueOf() + account.createdAt = createdAt + account.balance = new Decimal(0) + return account +} + +export const createGmwAccount = (keyPair: KeyPair, createdAt: Date): Account => { + return createAccount( + keyPair, + createdAt, + hardenDerivationIndex(GMW_ACCOUNT_DERIVATION_INDEX), + AddressType.COMMUNITY_GMW, + ) +} + +export const createAufAccount = (keyPair: KeyPair, createdAt: Date): Account => { + return createAccount( + keyPair, + createdAt, + hardenDerivationIndex(AUF_ACCOUNT_DERIVATION_INDEX), + AddressType.COMMUNITY_AUF, + ) +} diff --git a/dlt-connector/src/data/community.factory.ts b/dlt-connector/src/data/community.factory.ts new file mode 100644 index 000000000..d80ff315a --- /dev/null +++ b/dlt-connector/src/data/community.factory.ts @@ -0,0 +1,31 @@ +import { KeyManager } from '@/controller/KeyManager' +import { CommunityDraft } from '@/graphql/input/CommunityDraft' +import { iotaTopicFromCommunityUUID } from '@/utils/typeConverter' +import { Community } from '@entity/Community' +import { createAufAccount, createGmwAccount } from './account.factory' + +export const createCommunity = (communityDraft: CommunityDraft, topic?: string): Community => { + const communityEntity = Community.create() + communityEntity.iotaTopic = topic ?? iotaTopicFromCommunityUUID(communityDraft.uuid) + communityEntity.createdAt = new Date(communityDraft.createdAt) + communityEntity.foreign = communityDraft.foreign + return communityEntity +} + +export const createHomeCommunity = (communityDraft: CommunityDraft, topic?: string): Community => { + // create community entity + const community = createCommunity(communityDraft, topic) + + // generate key pair for signing transactions and deriving all keys for community + const keyPair = KeyManager.generateKeyPair() + community.rootPubkey = keyPair.publicKey + community.rootPrivkey = keyPair.privateKey + community.rootChaincode = keyPair.chainCode + // we should only have one home community per server + KeyManager.getInstance().setHomeCommunityKeyPair(keyPair) + + // create auf account and gmw account + community.aufAccount = createAufAccount(keyPair, community.createdAt) + community.gmwAccount = createGmwAccount(keyPair, community.createdAt) + return community +} diff --git a/dlt-connector/src/data/proto/3_3/CommunityRoot.ts b/dlt-connector/src/data/proto/3_3/CommunityRoot.ts new file mode 100644 index 000000000..401179d85 --- /dev/null +++ b/dlt-connector/src/data/proto/3_3/CommunityRoot.ts @@ -0,0 +1,40 @@ +import { TransactionBase } from '../TransactionBase' +import { TransactionValidationLevel } from '@/graphql/enum/TransactionValidationLevel' +import { Field, Message } from 'protobufjs' +import { Community } from '@entity/Community' +import { Transaction } from '@entity/Transaction' + +// https://www.npmjs.com/package/@apollo/protobufjs +// eslint-disable-next-line no-use-before-define +export class CommunityRoot extends Message implements TransactionBase { + 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-unused-vars + public validate(_level: TransactionValidationLevel): boolean { + throw new Error('Method not implemented.') + } + + // eslint-disable-next-line @typescript-eslint/no-empty-function, @typescript-eslint/no-unused-vars + public fillTransactionRecipe(recipe: Transaction): void {} +} diff --git a/dlt-connector/src/proto/3_3/GradidoConfirmedTransaction.ts b/dlt-connector/src/data/proto/3_3/ConfirmedTransaction.ts similarity index 67% rename from dlt-connector/src/proto/3_3/GradidoConfirmedTransaction.ts rename to dlt-connector/src/data/proto/3_3/ConfirmedTransaction.ts index 7f0a58109..ce88a54ae 100644 --- a/dlt-connector/src/proto/3_3/GradidoConfirmedTransaction.ts +++ b/dlt-connector/src/data/proto/3_3/ConfirmedTransaction.ts @@ -1,6 +1,8 @@ -import { Field, Message } from '@apollo/protobufjs' +import { Field, Message } from 'protobufjs' import { GradidoTransaction } from './GradidoTransaction' import { TimestampSeconds } from './TimestampSeconds' +import { base64ToBuffer } from '@/utils/typeConverter' +import Long from 'long' /* id will be set by Node server @@ -10,9 +12,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 { +export class ConfirmedTransaction extends Message { + 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 diff --git a/dlt-connector/src/proto/3_3/GradidoCreation.test.ts b/dlt-connector/src/data/proto/3_3/GradidoCreation.test.ts similarity index 100% rename from dlt-connector/src/proto/3_3/GradidoCreation.test.ts rename to dlt-connector/src/data/proto/3_3/GradidoCreation.test.ts diff --git a/dlt-connector/src/data/proto/3_3/GradidoCreation.ts b/dlt-connector/src/data/proto/3_3/GradidoCreation.ts new file mode 100644 index 000000000..354d40671 --- /dev/null +++ b/dlt-connector/src/data/proto/3_3/GradidoCreation.ts @@ -0,0 +1,53 @@ +import { Field, Message } from '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' +import { TransactionBase } from '../TransactionBase' +import { TransactionValidationLevel } from '@/graphql/enum/TransactionValidationLevel' +import { Transaction } from '@entity/Transaction' +import Decimal from 'decimal.js-light' +import { Account } from '@entity/Account' + +// 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 implements TransactionBase { + 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() + } + } + + @Field.d(1, TransferAmount) + public recipient: TransferAmount + + @Field.d(3, 'TimestampSeconds') + public targetDate: TimestampSeconds + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public validate(level: TransactionValidationLevel): boolean { + throw new Error('Method not implemented.') + } + + public fillTransactionRecipe(recipe: Transaction): void { + recipe.amount = new Decimal(this.recipient.amount ?? 0) + } +} diff --git a/dlt-connector/src/proto/3_3/GradidoDeferredTransfer.ts b/dlt-connector/src/data/proto/3_3/GradidoDeferredTransfer.ts similarity index 65% rename from dlt-connector/src/proto/3_3/GradidoDeferredTransfer.ts rename to dlt-connector/src/data/proto/3_3/GradidoDeferredTransfer.ts index 7b27c064a..4693df879 100644 --- a/dlt-connector/src/proto/3_3/GradidoDeferredTransfer.ts +++ b/dlt-connector/src/data/proto/3_3/GradidoDeferredTransfer.ts @@ -1,7 +1,11 @@ -import { Field, Message } from '@apollo/protobufjs' +import { Field, Message } from 'protobufjs' import { GradidoTransfer } from './GradidoTransfer' import { TimestampSeconds } from './TimestampSeconds' +import { TransactionBase } from '../TransactionBase' +import { TransactionValidationLevel } from '@/graphql/enum/TransactionValidationLevel' +import { Transaction } from '@entity/Transaction' +import Decimal from 'decimal.js-light' // transaction type for chargeable transactions // for transaction for people which haven't a account already @@ -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 { +export class GradidoDeferredTransfer + // eslint-disable-next-line no-use-before-define + extends Message + implements TransactionBase +{ // 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,13 @@ export class GradidoDeferredTransfer extends Message { // split for n recipient // max gradido per recipient? or per transaction with cool down? + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public validate(level: TransactionValidationLevel): boolean { + throw new Error('Method not implemented.') + } + + public fillTransactionRecipe(recipe: Transaction): void { + recipe.amount = new Decimal(this.transfer.sender.amount ?? 0) + } } diff --git a/dlt-connector/src/data/proto/3_3/GradidoTransaction.ts b/dlt-connector/src/data/proto/3_3/GradidoTransaction.ts new file mode 100644 index 000000000..d4eea4836 --- /dev/null +++ b/dlt-connector/src/data/proto/3_3/GradidoTransaction.ts @@ -0,0 +1,43 @@ +import { Field, Message } from 'protobufjs' + +import { SignatureMap } from './SignatureMap' +import { TransactionBody } from './TransactionBody' +import { SignaturePair } from './SignaturePair' +import { LogError } from '@/server/LogError' + +// https://www.npmjs.com/package/@apollo/protobufjs +// eslint-disable-next-line no-use-before-define +export class GradidoTransaction extends Message { + 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] + } +} diff --git a/dlt-connector/src/data/proto/3_3/GradidoTransfer.ts b/dlt-connector/src/data/proto/3_3/GradidoTransfer.ts new file mode 100644 index 000000000..dadddedec --- /dev/null +++ b/dlt-connector/src/data/proto/3_3/GradidoTransfer.ts @@ -0,0 +1,48 @@ +import { Field, Message } from 'protobufjs' + +import { TransferAmount } from './TransferAmount' +import { TransactionDraft } from '@/graphql/input/TransactionDraft' +import { TransactionBase } from '../TransactionBase' +import { TransactionValidationLevel } from '@/graphql/enum/TransactionValidationLevel' +import { Transaction } from '@entity/Transaction' +import Decimal from 'decimal.js-light' +import { Account } from '@entity/Account' + +// https://www.npmjs.com/package/@apollo/protobufjs +// eslint-disable-next-line no-use-before-define +export class GradidoTransfer extends Message implements TransactionBase { + 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() + } + } + + @Field.d(1, TransferAmount) + public sender: TransferAmount + + @Field.d(2, 'bytes') + public recipient: Buffer + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public validate(level: TransactionValidationLevel): boolean { + throw new Error('Method not implemented.') + } + + public fillTransactionRecipe(recipe: Transaction): void { + recipe.amount = new Decimal(this.sender?.amount ?? 0) + } +} diff --git a/dlt-connector/src/proto/3_3/GroupFriendsUpdate.ts b/dlt-connector/src/data/proto/3_3/GroupFriendsUpdate.ts similarity index 52% rename from dlt-connector/src/proto/3_3/GroupFriendsUpdate.ts rename to dlt-connector/src/data/proto/3_3/GroupFriendsUpdate.ts index 64e74a694..3d6811331 100644 --- a/dlt-connector/src/proto/3_3/GroupFriendsUpdate.ts +++ b/dlt-connector/src/data/proto/3_3/GroupFriendsUpdate.ts @@ -1,10 +1,14 @@ -import { Field, Message } from '@apollo/protobufjs' +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { TransactionBase } from '../TransactionBase' +import { TransactionValidationLevel } from '@/graphql/enum/TransactionValidationLevel' +import { Field, Message } from 'protobufjs' +import { Transaction } from '@entity/Transaction' // 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 { +export class GroupFriendsUpdate extends Message implements TransactionBase { // 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,12 @@ export class GroupFriendsUpdate extends Message { // (if fusion between src coin and dst coin is enabled) @Field.d(1, 'bool') public colorFusion: boolean + + public validate(level: TransactionValidationLevel): boolean { + throw new Error('Method not implemented.') + } + + public fillTransactionRecipe(recipe: Transaction): void { + throw new Error('Method not implemented.') + } } diff --git a/dlt-connector/src/data/proto/3_3/RegisterAddress.ts b/dlt-connector/src/data/proto/3_3/RegisterAddress.ts new file mode 100644 index 000000000..f9eead762 --- /dev/null +++ b/dlt-connector/src/data/proto/3_3/RegisterAddress.ts @@ -0,0 +1,33 @@ +/* eslint-disable @typescript-eslint/no-empty-function */ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { Field, Message } from 'protobufjs' + +import { AddressType } from '@/data/proto/3_3/enum/AddressType' +import { TransactionBase } from '../TransactionBase' +import { TransactionValidationLevel } from '@/graphql/enum/TransactionValidationLevel' +import { Transaction } from '@entity/Transaction' + +// https://www.npmjs.com/package/@apollo/protobufjs +// eslint-disable-next-line no-use-before-define +export class RegisterAddress extends Message implements TransactionBase { + @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 validate(level: TransactionValidationLevel): boolean { + throw new Error('Method not implemented.') + } + + public fillTransactionRecipe(_recipe: Transaction): void {} +} diff --git a/dlt-connector/src/proto/3_3/SignatureMap.ts b/dlt-connector/src/data/proto/3_3/SignatureMap.ts similarity index 66% rename from dlt-connector/src/proto/3_3/SignatureMap.ts rename to dlt-connector/src/data/proto/3_3/SignatureMap.ts index e48b0232d..daf69f05f 100644 --- a/dlt-connector/src/proto/3_3/SignatureMap.ts +++ b/dlt-connector/src/data/proto/3_3/SignatureMap.ts @@ -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 { + constructor() { + super({ sigPair: [] }) + } + @Field.d(1, SignaturePair, 'repeated') - public sigPair: SignaturePair + public sigPair: SignaturePair[] } diff --git a/dlt-connector/src/proto/3_3/SignaturePair.ts b/dlt-connector/src/data/proto/3_3/SignaturePair.ts similarity index 63% rename from dlt-connector/src/proto/3_3/SignaturePair.ts rename to dlt-connector/src/data/proto/3_3/SignaturePair.ts index 07ed4cc55..80a61a871 100644 --- a/dlt-connector/src/proto/3_3/SignaturePair.ts +++ b/dlt-connector/src/data/proto/3_3/SignaturePair.ts @@ -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 { @Field.d(2, 'bytes') public signature: Buffer + + public validate(): boolean { + return this.pubKey.length === 32 && this.signature.length === 64 + } } diff --git a/dlt-connector/src/proto/3_3/Timestamp.test.ts b/dlt-connector/src/data/proto/3_3/Timestamp.test.ts similarity index 100% rename from dlt-connector/src/proto/3_3/Timestamp.test.ts rename to dlt-connector/src/data/proto/3_3/Timestamp.test.ts diff --git a/dlt-connector/src/proto/3_3/Timestamp.ts b/dlt-connector/src/data/proto/3_3/Timestamp.ts similarity index 94% rename from dlt-connector/src/proto/3_3/Timestamp.ts rename to dlt-connector/src/data/proto/3_3/Timestamp.ts index ab060a9bc..91cf06581 100644 --- a/dlt-connector/src/proto/3_3/Timestamp.ts +++ b/dlt-connector/src/data/proto/3_3/Timestamp.ts @@ -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 diff --git a/dlt-connector/src/proto/3_3/TimestampSeconds.test.ts b/dlt-connector/src/data/proto/3_3/TimestampSeconds.test.ts similarity index 100% rename from dlt-connector/src/proto/3_3/TimestampSeconds.test.ts rename to dlt-connector/src/data/proto/3_3/TimestampSeconds.test.ts diff --git a/dlt-connector/src/proto/3_3/TimestampSeconds.ts b/dlt-connector/src/data/proto/3_3/TimestampSeconds.ts similarity index 91% rename from dlt-connector/src/proto/3_3/TimestampSeconds.ts rename to dlt-connector/src/data/proto/3_3/TimestampSeconds.ts index 055094c6d..6d175c6f3 100644 --- a/dlt-connector/src/proto/3_3/TimestampSeconds.ts +++ b/dlt-connector/src/data/proto/3_3/TimestampSeconds.ts @@ -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 diff --git a/dlt-connector/src/data/proto/3_3/TransactionBody.ts b/dlt-connector/src/data/proto/3_3/TransactionBody.ts new file mode 100644 index 000000000..8bfbf955f --- /dev/null +++ b/dlt-connector/src/data/proto/3_3/TransactionBody.ts @@ -0,0 +1,129 @@ +import { Field, Message, OneOf } from 'protobufjs' + +import { CrossGroupType } from './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 '../transactionBody.logic' +import { CommunityRoot } from './CommunityRoot' +import { CommunityDraft } from '@/graphql/input/CommunityDraft' +import { TransactionType } from '@/graphql/enum/TransactionType' +import { TransactionBase } from '../TransactionBase' +import { Transaction } from '@entity/Transaction' +import { timestampToDate } from '@/utils/typeConverter' +import { LogError } from '@/server/LogError' + +// https://www.npmjs.com/package/@apollo/protobufjs +// eslint-disable-next-line no-use-before-define +export class TransactionBody extends Message { + public constructor(transaction?: TransactionDraft | CommunityDraft) { + if (transaction) { + let type = CrossGroupType.LOCAL + let otherGroup = '' + if (transaction instanceof TransactionDraft) { + type = determineCrossGroupType(transaction) + otherGroup = determineOtherGroup(type, transaction) + } + + super({ + memo: 'Not implemented yet', + createdAt: new Timestamp(new Date(transaction.createdAt)), + versionNumber: '3.3', + type, + 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 + } + + public getTransactionBase(): TransactionBase | 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.getTransactionBase()?.fillTransactionRecipe(recipe) + } + + public getRecipientPublicKey(): Buffer | undefined { + if (this.transfer) { + return this.transfer.recipient + } + if (this.creation) { + return this.creation.recipient.pubkey + } + if (this.deferredTransfer) { + return this.deferredTransfer.transfer.recipient + } + return undefined + } +} diff --git a/dlt-connector/src/proto/3_3/TransferAmount.ts b/dlt-connector/src/data/proto/3_3/TransferAmount.ts similarity index 88% rename from dlt-connector/src/proto/3_3/TransferAmount.ts rename to dlt-connector/src/data/proto/3_3/TransferAmount.ts index f6adc47ff..42da65256 100644 --- a/dlt-connector/src/proto/3_3/TransferAmount.ts +++ b/dlt-connector/src/data/proto/3_3/TransferAmount.ts @@ -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 diff --git a/dlt-connector/src/data/proto/3_3/enum/AddressType.ts b/dlt-connector/src/data/proto/3_3/enum/AddressType.ts new file mode 100644 index 000000000..49fcf0c4e --- /dev/null +++ b/dlt-connector/src/data/proto/3_3/enum/AddressType.ts @@ -0,0 +1,19 @@ +export enum AddressType { + NONE = 0, // if no address was found + COMMUNITY_HUMAN = 1, // creation account for human + COMMUNITY_GMW = 2, // community public budget account + COMMUNITY_AUF = 3, // community compensation and environment founds account + COMMUNITY_PROJECT = 4, // no creations allowed + SUBACCOUNT = 5, // no creations allowed + CRYPTO_ACCOUNT = 6, // user control his keys, no creations +} + +export function getAddressTypeEnumValue(typeString: string): AddressType | undefined { + // Iterate through all enum values + for (const key in AddressType) { + if (AddressType[key] === typeString) { + return AddressType[key] as unknown as AddressType + } + } + return undefined // If the string is not found +} diff --git a/dlt-connector/src/data/proto/3_3/enum/CrossGroupType.ts b/dlt-connector/src/data/proto/3_3/enum/CrossGroupType.ts new file mode 100644 index 000000000..13e968509 --- /dev/null +++ b/dlt-connector/src/data/proto/3_3/enum/CrossGroupType.ts @@ -0,0 +1,7 @@ +export enum CrossGroupType { + LOCAL = 0, + INBOUND = 1, + OUTBOUND = 2, + // for cross group transaction which haven't a direction like group friend update + // CROSS = 3, +} diff --git a/dlt-connector/src/controller/TransactionBase.ts b/dlt-connector/src/data/proto/TransactionBase.ts similarity index 73% rename from dlt-connector/src/controller/TransactionBase.ts rename to dlt-connector/src/data/proto/TransactionBase.ts index 9833226a9..e04b3435e 100644 --- a/dlt-connector/src/controller/TransactionBase.ts +++ b/dlt-connector/src/data/proto/TransactionBase.ts @@ -1,6 +1,9 @@ import { TransactionValidationLevel } from '@/graphql/enum/TransactionValidationLevel' +import { Transaction } from '@entity/Transaction' 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 + + public abstract fillTransactionRecipe(recipe: Transaction): void } diff --git a/dlt-connector/src/data/proto/TransactionBody.builder.ts b/dlt-connector/src/data/proto/TransactionBody.builder.ts new file mode 100644 index 000000000..5f2681824 --- /dev/null +++ b/dlt-connector/src/data/proto/TransactionBody.builder.ts @@ -0,0 +1,95 @@ +import { TransactionBody } from './3_3/TransactionBody' +import { Account } from '@entity/Account' +import { TransactionDraft } from '@/graphql/input/TransactionDraft' +import { CommunityDraft } from '@/graphql/input/CommunityDraft' +import { InputTransactionType } from '@/graphql/enum/InputTransactionType' +import { GradidoCreation } from './3_3/GradidoCreation' +import { GradidoTransfer } from './3_3/GradidoTransfer' +import { Community } from '@entity/Community' +import { CommunityRoot } from './3_3/CommunityRoot' +import { LogError } from '@/server/LogError' + +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 + } + + /** + * 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.body + if (!result) { + throw new LogError( + 'cannot build Transaction Body, missing information, please call at least fromTransactionDraft or fromCommunityDraft', + ) + } + this.reset() + return result + } + + public setSigningAccount(signingAccount: Account): TransactionBodyBuilder { + this.signingAccount = signingAccount + return this + } + + public setRecipientAccount(recipientAccount: Account): TransactionBodyBuilder { + this.recipientAccount = recipientAccount + 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: + this.body.creation = new GradidoCreation(transactionDraft, this.recipientAccount) + this.body.data = 'gradidoCreation' + break + case InputTransactionType.SEND: + case InputTransactionType.RECEIVE: + 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 + } +} diff --git a/dlt-connector/src/controller/TransactionBody.ts b/dlt-connector/src/data/proto/transactionBody.logic.ts similarity index 60% rename from dlt-connector/src/controller/TransactionBody.ts rename to dlt-connector/src/data/proto/transactionBody.logic.ts index ae5f37710..872172aac 100644 --- a/dlt-connector/src/controller/TransactionBody.ts +++ b/dlt-connector/src/data/proto/transactionBody.logic.ts @@ -1,28 +1,8 @@ -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 { CrossGroupType } from './3_3/enum/CrossGroupType' +import { InputTransactionType } from '@/graphql/enum/InputTransactionType' 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 -} +import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType' export const determineCrossGroupType = ({ senderUser, @@ -33,12 +13,12 @@ export const determineCrossGroupType = ({ !recipientUser.communityUuid || recipientUser.communityUuid === '' || senderUser.communityUuid === recipientUser.communityUuid || - type === TransactionType.CREATION + type === InputTransactionType.CREATION ) { return CrossGroupType.LOCAL - } else if (type === TransactionType.SEND) { + } else if (type === InputTransactionType.SEND) { return CrossGroupType.INBOUND - } else if (type === TransactionType.RECEIVE) { + } else if (type === InputTransactionType.RECEIVE) { return CrossGroupType.OUTBOUND } throw new TransactionError( diff --git a/dlt-connector/src/graphql/arg/CommunityArg.ts b/dlt-connector/src/graphql/arg/CommunityArg.ts new file mode 100644 index 000000000..59f65943e --- /dev/null +++ b/dlt-connector/src/graphql/arg/CommunityArg.ts @@ -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 +} diff --git a/dlt-connector/src/graphql/enum/InputTransactionType.ts b/dlt-connector/src/graphql/enum/InputTransactionType.ts new file mode 100755 index 000000000..06d1bb44b --- /dev/null +++ b/dlt-connector/src/graphql/enum/InputTransactionType.ts @@ -0,0 +1,12 @@ +import { registerEnumType } from 'type-graphql' + +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 +}) diff --git a/dlt-connector/src/graphql/enum/TransactionErrorType.ts b/dlt-connector/src/graphql/enum/TransactionErrorType.ts index 0e72292c1..7f1902c3d 100644 --- a/dlt-connector/src/graphql/enum/TransactionErrorType.ts +++ b/dlt-connector/src/graphql/enum/TransactionErrorType.ts @@ -5,6 +5,8 @@ export enum TransactionErrorType { MISSING_PARAMETER = 'Missing parameter', ALREADY_EXIST = 'Already exist', DB_ERROR = 'DB Error', + PROTO_DECODE_ERROR = 'Proto Decode Error', + PROTO_ENCODE_ERROR = 'Proto Encode Error', } registerEnumType(TransactionErrorType, { diff --git a/dlt-connector/src/graphql/enum/TransactionType.ts b/dlt-connector/src/graphql/enum/TransactionType.ts old mode 100755 new mode 100644 index aaa5bf92e..3647b9ca8 --- a/dlt-connector/src/graphql/enum/TransactionType.ts +++ b/dlt-connector/src/graphql/enum/TransactionType.ts @@ -1,12 +1,15 @@ -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 -}) +import { registerEnumType } from 'type-graphql' + +export enum TransactionType { + GRADIDO_TRANSFER = 1, + GRADIDO_CREATION = 2, + GROUP_FRIENDS_UPDATE = 3, + REGISTER_ADDRESS = 4, + GRADIDO_DEFERRED_TRANSFER = 5, + COMMUNITY_ROOT = 6, +} + +registerEnumType(TransactionType, { + name: 'TransactionType', // this one is mandatory + description: 'Type of the transaction', // this one is optional +}) diff --git a/dlt-connector/src/graphql/input/TransactionDraft.ts b/dlt-connector/src/graphql/input/TransactionDraft.ts index 2021dd9e1..a94c57354 100755 --- a/dlt-connector/src/graphql/input/TransactionDraft.ts +++ b/dlt-connector/src/graphql/input/TransactionDraft.ts @@ -1,7 +1,7 @@ // https://www.npmjs.com/package/@apollo/protobufjs import { Decimal } from 'decimal.js-light' -import { TransactionType } from '@enum/TransactionType' +import { InputTransactionType } from '@enum/InputTransactionType' import { InputType, Field } from 'type-graphql' import { UserIdentifier } from './UserIdentifier' import { isValidDateString } from '@validator/DateString' @@ -24,9 +24,9 @@ export class TransactionDraft { @IsPositiveDecimal() amount: Decimal - @Field(() => TransactionType) - @IsEnum(TransactionType) - type: TransactionType + @Field(() => InputTransactionType) + @IsEnum(InputTransactionType) + type: InputTransactionType @Field(() => String) @isValidDateString() diff --git a/dlt-connector/src/graphql/model/Community.ts b/dlt-connector/src/graphql/model/Community.ts new file mode 100644 index 000000000..214984f15 --- /dev/null +++ b/dlt-connector/src/graphql/model/Community.ts @@ -0,0 +1,36 @@ +import { ObjectType, Field, Int } from 'type-graphql' +import { Community as CommunityEntity } from '@entity/Community' + +@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 +} diff --git a/dlt-connector/src/graphql/model/TransactionRecipe.ts b/dlt-connector/src/graphql/model/TransactionRecipe.ts new file mode 100644 index 000000000..e5e1c74fc --- /dev/null +++ b/dlt-connector/src/graphql/model/TransactionRecipe.ts @@ -0,0 +1,25 @@ +import { Field, Int, ObjectType } from 'type-graphql' +import { TransactionRecipe as TransactionRecipeEntity } from '@entity/TransactionRecipe' +import { TransactionType } from '@enum/TransactionType' + +@ObjectType() +export class TransactionRecipe { + public constructor({ id, createdAt, type, senderCommunity }: TransactionRecipeEntity) { + this.id = id + this.createdAt = createdAt.toString() + this.type = type + this.topic = senderCommunity.iotaTopic + } + + @Field(() => Int) + id: number + + @Field(() => String) + createdAt: string + + @Field(() => TransactionType) + type: TransactionType + + @Field(() => String) + topic: string +} diff --git a/dlt-connector/src/graphql/model/TransactionResult.ts b/dlt-connector/src/graphql/model/TransactionResult.ts index 938c8bcf6..dfa28cf1b 100644 --- a/dlt-connector/src/graphql/model/TransactionResult.ts +++ b/dlt-connector/src/graphql/model/TransactionResult.ts @@ -1,15 +1,16 @@ 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,9 +19,9 @@ 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 -} +} \ No newline at end of file diff --git a/dlt-connector/src/graphql/resolver/CommunityResolver.ts b/dlt-connector/src/graphql/resolver/CommunityResolver.ts index d6f9f2d46..43a871dd8 100644 --- a/dlt-connector/src/graphql/resolver/CommunityResolver.ts +++ b/dlt-connector/src/graphql/resolver/CommunityResolver.ts @@ -1,53 +1,59 @@ -import { Resolver, Arg, Mutation } from 'type-graphql' +import { Resolver, Query, Arg, Mutation, Args } from 'type-graphql' import { CommunityDraft } from '@input/CommunityDraft' -import { TransactionResult } from '../model/TransactionResult' -import { TransactionError } from '../model/TransactionError' -import { create as createCommunity, isExist } from '@/controller/Community' -import { TransactionErrorType } from '../enum/TransactionErrorType' -import { logger } from '@/server/logger' +import { TransactionResult } from '@model/TransactionResult' +import { TransactionError } from '@model/TransactionError' +import { TransactionErrorType } from '@enum/TransactionErrorType' import { iotaTopicFromCommunityUUID } from '@/utils/typeConverter' +import { Community } from '@model/Community' +import { CommunityArg } from '@arg/CommunityArg' +import { LogError } from '@/server/LogError' +import { logger } from '@/server/logger' +import { CommunityRepository } from '@/data/Community.repository' +import { addCommunity } from '@/interactions/backendToDb/community/community.context' @Resolver() export class CommunityResolver { + @Query(() => Community) + async community(@Args() communityArg: CommunityArg): Promise { + 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 { + logger.info('isCommunity', communityArg) + return (await CommunityRepository.findByCommunityArg(communityArg)).length === 1 + } + + @Query(() => [Community]) + async communities(@Args() communityArg: CommunityArg): Promise { + 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 { - try { - const topic = iotaTopicFromCommunityUUID(communityDraft.uuid) - - // check if community was already written to db - if (await isExist(topic)) { - return new TransactionResult( - new TransactionError(TransactionErrorType.ALREADY_EXIST, 'community already exist!'), - ) - } - const community = createCommunity(communityDraft, topic) - - let result: TransactionResult - - if (!communityDraft.foreign) { - // TODO: CommunityRoot Transaction for blockchain - } - try { - await community.save() - result = new TransactionResult() - } catch (err) { - logger.error('error saving new community into db: %s', err) - result = new TransactionResult( - new TransactionError(TransactionErrorType.DB_ERROR, 'error saving community into db'), - ) - } - return result - } catch (error) { - if (error instanceof TransactionError) { - return new TransactionResult(error) - } else { - throw error - } + 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!'), + ) } + return await addCommunity(communityDraft, topic) } } diff --git a/dlt-connector/src/graphql/resolver/TransactionsResolver.ts b/dlt-connector/src/graphql/resolver/TransactionsResolver.ts index 282eb11cd..96f6dff65 100755 --- a/dlt-connector/src/graphql/resolver/TransactionsResolver.ts +++ b/dlt-connector/src/graphql/resolver/TransactionsResolver.ts @@ -1,12 +1,5 @@ import { Resolver, Query, 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 { sendMessage as iotaSendMessage } from '@/client/IotaClient' -import { GradidoTransaction } from '@/proto/3_3/GradidoTransaction' import { TransactionResult } from '../model/TransactionResult' import { TransactionError } from '../model/TransactionError' @@ -29,11 +22,68 @@ export class TransactionResolver { transaction: TransactionDraft, ): Promise { 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) + logger.info('sendTransaction call', transaction) + const signingAccount = await findAccountByUserIdentifier(transaction.senderUser) + if (!signingAccount) { + throw new TransactionError( + TransactionErrorType.NOT_FOUND, + "couldn't found sender user account in db", + ) + } + logger.info('signing account', signingAccount) + + const recipientAccount = await findAccountByUserIdentifier(transaction.recipientUser) + if (!recipientAccount) { + throw new TransactionError( + TransactionErrorType.NOT_FOUND, + "couldn't found recipient user account in db", + ) + } + logger.info('recipient account', recipientAccount) + + const body = createTransactionBody(transaction, signingAccount, recipientAccount) + logger.info('body', body) + const gradidoTransaction = createGradidoTransaction(body) + + const signingKeyPair = getKeyPair(signingAccount) + if (!signingKeyPair) { + throw new TransactionError( + TransactionErrorType.NOT_FOUND, + "couldn't found signing key pair", + ) + } + logger.info('key pair for signing', signingKeyPair) + + KeyManager.getInstance().sign(gradidoTransaction, [signingKeyPair]) + const recipeTransactionController = await TransactionRecipe.create({ + transaction: gradidoTransaction, + senderUser: transaction.senderUser, + recipientUser: transaction.recipientUser, + signingAccount, + recipientAccount, + backendTransactionId: transaction.backendTransactionId, + }) + try { + await recipeTransactionController.getTransactionRecipeEntity().save() + ConditionalSleepManager.getInstance().signal(TRANSMIT_TO_IOTA_SLEEP_CONDITION_KEY) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (error: any) { + if (error.code === 'ER_DUP_ENTRY' && body.type === CrossGroupType.LOCAL) { + const existingRecipe = await findBySignature( + gradidoTransaction.sigMap.sigPair[0].signature, + ) + if (!existingRecipe) { + throw new TransactionError( + TransactionErrorType.LOGIC_ERROR, + "recipe cannot be added because signature exist but couldn't load this existing receipt", + ) + } + return new TransactionResult(new TransactionRecipeOutput(existingRecipe)) + } else { + throw error + } + } + return new TransactionResult() } catch (error) { if (error instanceof TransactionError) { return new TransactionResult(error) diff --git a/dlt-connector/src/interactions/backendToDb/community/Community.role.ts b/dlt-connector/src/interactions/backendToDb/community/Community.role.ts new file mode 100644 index 000000000..144d3bc5c --- /dev/null +++ b/dlt-connector/src/interactions/backendToDb/community/Community.role.ts @@ -0,0 +1,6 @@ +import { CommunityDraft } from '@/graphql/input/CommunityDraft' +import { Community } from '@entity/Community' + +export abstract class CommunityRole { + abstract addCommunity(communityDraft: CommunityDraft, topic: string): Promise +} diff --git a/dlt-connector/src/interactions/backendToDb/community/ForeignCommunity.role.ts b/dlt-connector/src/interactions/backendToDb/community/ForeignCommunity.role.ts new file mode 100644 index 000000000..76af3026c --- /dev/null +++ b/dlt-connector/src/interactions/backendToDb/community/ForeignCommunity.role.ts @@ -0,0 +1,19 @@ +import { CommunityDraft } from '@/graphql/input/CommunityDraft' +import { Community } from '@entity/Community' +import { CommunityRole } from './Community.role' +import { logger } from '@/server/logger' +import { TransactionError } from '@/graphql/model/TransactionError' +import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType' +import { createCommunity } from '@/data/community.factory' + +export class ForeignCommunityRole extends CommunityRole { + addCommunity(communityDraft: CommunityDraft, topic: string): Promise { + const community = createCommunity(communityDraft, topic) + try { + return community.save() + } catch (error) { + logger.error('error saving new foreign community into db: %s', error) + throw new TransactionError(TransactionErrorType.DB_ERROR, 'error saving community into db') + } + } +} diff --git a/dlt-connector/src/interactions/backendToDb/community/HomeCommunity.role.ts b/dlt-connector/src/interactions/backendToDb/community/HomeCommunity.role.ts new file mode 100644 index 000000000..df670398f --- /dev/null +++ b/dlt-connector/src/interactions/backendToDb/community/HomeCommunity.role.ts @@ -0,0 +1,28 @@ +import { CommunityDraft } from '@/graphql/input/CommunityDraft' +import { Community } from '@entity/Community' +import { CommunityRole } from './Community.role' +import { getTransaction } from '@/client/GradidoNode' +import { timestampSecondsToDate } from '@/utils/typeConverter' +import { createHomeCommunity } from '@/data/community.factory' +import { createCommunityRootTransactionRecipe } from '../transaction/transaction.context' +import { QueryRunner } from 'typeorm' + +export class HomeCommunityRole extends CommunityRole { + public async addCommunity(communityDraft: CommunityDraft, topic: string): Promise { + const community = createHomeCommunity(communityDraft, topic) + + // check if a CommunityRoot Transaction exist already on iota blockchain + const existingCommunityRootTransaction = await getTransaction(1, community.iotaTopic) + if (existingCommunityRootTransaction) { + community.confirmedAt = timestampSecondsToDate(existingCommunityRootTransaction.confirmedAt) + return community.save() + } else { + createCommunityRootTransactionRecipe(communityDraft, community).storeAsTransaction( + async (queryRunner: QueryRunner): Promise => { + await queryRunner.manager.save(community) + }, + ) + } + return community.save() + } +} diff --git a/dlt-connector/src/interactions/backendToDb/community/community.context.ts b/dlt-connector/src/interactions/backendToDb/community/community.context.ts new file mode 100644 index 000000000..cd55599bf --- /dev/null +++ b/dlt-connector/src/interactions/backendToDb/community/community.context.ts @@ -0,0 +1,30 @@ +import { CommunityDraft } from '@/graphql/input/CommunityDraft' +import { TransactionResult } from '@/graphql/model/TransactionResult' +import { ForeignCommunityRole } from './ForeignCommunity.role' +import { HomeCommunityRole } from './HomeCommunity.role' +import { TransactionError } from '@/graphql/model/TransactionError' +import { TransactionsManager } from '@/controller/TransactionsManager' +import { iotaTopicFromCommunityUUID } from '@/utils/typeConverter' + +export const addCommunity = async ( + communityDraft: CommunityDraft, + iotaTopic?: string, +): Promise => { + const communityRole = communityDraft.foreign + ? new ForeignCommunityRole() + : new HomeCommunityRole() + try { + if (!iotaTopic) { + iotaTopic = iotaTopicFromCommunityUUID(communityDraft.uuid) + } + await communityRole.addCommunity(communityDraft, iotaTopic) + await TransactionsManager.getInstance().addTopic(iotaTopic) + return new TransactionResult() + } catch (error) { + if (error instanceof TransactionError) { + return new TransactionResult(error) + } else { + throw error + } + } +} diff --git a/dlt-connector/src/interactions/backendToDb/transaction/CommunityRootTransaction.role.ts b/dlt-connector/src/interactions/backendToDb/transaction/CommunityRootTransaction.role.ts new file mode 100644 index 000000000..3bce63461 --- /dev/null +++ b/dlt-connector/src/interactions/backendToDb/transaction/CommunityRootTransaction.role.ts @@ -0,0 +1,24 @@ +import { CommunityDraft } from '@/graphql/input/CommunityDraft' +import { TransactionRecipeRole } from './TransactionRecipe.role' +import { Community } from '@entity/Community' +import { TransactionBodyBuilder } from '@/data/proto/TransactionBody.builder' +import { KeyPair } from '@/data/KeyPair' +import { sign } from '@/utils/cryptoHelper' + +export class CommunityRootTransactionRole extends TransactionRecipeRole { + public createFromCommunityDraft( + communityDraft: CommunityDraft, + community: Community, + ): CommunityRootTransactionRole { + // create proto transaction body + const transactionBody = new TransactionBodyBuilder() + .fromCommunityDraft(communityDraft, community) + .build() + // build transaction entity + this.transactionBuilder.fromTransactionBody(transactionBody).setSenderCommunity(community) + const transaction = this.transactionBuilder.getTransaction() + // sign + this.transactionBuilder.setSignature(sign(transaction.bodyBytes, new KeyPair(community))) + return this + } +} diff --git a/dlt-connector/src/interactions/backendToDb/transaction/TransactionRecipe.role.ts b/dlt-connector/src/interactions/backendToDb/transaction/TransactionRecipe.role.ts new file mode 100644 index 000000000..0ab41f0d4 --- /dev/null +++ b/dlt-connector/src/interactions/backendToDb/transaction/TransactionRecipe.role.ts @@ -0,0 +1,49 @@ +import { TransactionBuilder } from '@/data/Transaction.builder' +import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType' +import { TransactionDraft } from '@/graphql/input/TransactionDraft' +import { TransactionError } from '@/graphql/model/TransactionError' +import { TransactionRecipe } from '@/graphql/model/TransactionRecipe' +import { TransactionResult } from '@/graphql/model/TransactionResult' +import { logger } from '@/server/logger' +import { getDataSource } from '@/typeorm/DataSource' +import { QueryRunner } from 'typeorm' + +export class TransactionRecipeRole { + protected transactionBuilder: TransactionBuilder + construct() { + this.transactionBuilder = new TransactionBuilder() + } + + public createFromTransactionDraft(transactionDraft: TransactionDraft): TransactionRecipeRole { + return this + } + + public async storeAsTransaction( + transactionFunction: (queryRunner: QueryRunner) => Promise, + ): Promise { + const queryRunner = getDataSource().createQueryRunner() + await queryRunner.connect() + await queryRunner.startTransaction() + + let result: TransactionResult + try { + const transactionRecipe = this.transactionBuilder.build() + await transactionFunction(queryRunner) + await queryRunner.manager.save(transactionRecipe) + await queryRunner.commitTransaction() + result = new TransactionResult(new TransactionRecipe(transactionRecipe)) + } catch (err) { + logger.error('error saving new transaction recipe into db: %s', err) + result = new TransactionResult( + new TransactionError( + TransactionErrorType.DB_ERROR, + 'error saving transaction recipe into db', + ), + ) + await queryRunner.rollbackTransaction() + } finally { + await queryRunner.release() + } + return result + } +} diff --git a/dlt-connector/src/interactions/backendToDb/transaction/transaction.context.ts b/dlt-connector/src/interactions/backendToDb/transaction/transaction.context.ts new file mode 100644 index 000000000..5c8bda04f --- /dev/null +++ b/dlt-connector/src/interactions/backendToDb/transaction/transaction.context.ts @@ -0,0 +1,20 @@ +import { CommunityDraft } from '@/graphql/input/CommunityDraft' +import { Community } from '@entity/Community' +import { TransactionRecipeRole } from './TransactionRecipe.role' +import { CommunityRootTransactionRole } from './CommunityRootTransaction.role' +import { TransactionDraft } from '@/graphql/input/TransactionDraft' + +export const createCommunityRootTransactionRecipe = ( + communityDraft: CommunityDraft, + community: Community, +): TransactionRecipeRole => { + const communityRootTransactionRole = new CommunityRootTransactionRole() + return communityRootTransactionRole.createFromCommunityDraft(communityDraft, community) +} + +export const createTransactionRecipe = ( + transactionDraft: TransactionDraft, +): TransactionRecipeRole => { + const transactionRecipeRole = new TransactionRecipeRole() + return transactionRecipeRole.createFromTransactionDraft(transactionDraft) +} diff --git a/dlt-connector/src/proto/3_3/GradidoCreation.ts b/dlt-connector/src/proto/3_3/GradidoCreation.ts deleted file mode 100644 index ba6e93652..000000000 --- a/dlt-connector/src/proto/3_3/GradidoCreation.ts +++ /dev/null @@ -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 { - 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 -} diff --git a/dlt-connector/src/proto/3_3/GradidoTransaction.ts b/dlt-connector/src/proto/3_3/GradidoTransaction.ts deleted file mode 100644 index ca1a59e30..000000000 --- a/dlt-connector/src/proto/3_3/GradidoTransaction.ts +++ /dev/null @@ -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 { - @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 -} diff --git a/dlt-connector/src/proto/3_3/GradidoTransfer.ts b/dlt-connector/src/proto/3_3/GradidoTransfer.ts deleted file mode 100644 index 215ffc60f..000000000 --- a/dlt-connector/src/proto/3_3/GradidoTransfer.ts +++ /dev/null @@ -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 { - 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 -} diff --git a/dlt-connector/src/proto/3_3/RegisterAddress.ts b/dlt-connector/src/proto/3_3/RegisterAddress.ts deleted file mode 100644 index 85b8390df..000000000 --- a/dlt-connector/src/proto/3_3/RegisterAddress.ts +++ /dev/null @@ -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 { - @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 -} diff --git a/dlt-connector/src/proto/3_3/TransactionBody.ts b/dlt-connector/src/proto/3_3/TransactionBody.ts deleted file mode 100644 index 9e9179b3f..000000000 --- a/dlt-connector/src/proto/3_3/TransactionBody.ts +++ /dev/null @@ -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 { - 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 -} diff --git a/dlt-connector/src/utils/cryptoHelper.ts b/dlt-connector/src/utils/cryptoHelper.ts new file mode 100644 index 000000000..1eb5db36d --- /dev/null +++ b/dlt-connector/src/utils/cryptoHelper.ts @@ -0,0 +1,6 @@ +import { KeyPair } from '@/data/KeyPair' +import { sign as ed25519Sign } from 'bip32-ed25519' + +export const sign = (message: Buffer, keyPair: KeyPair): Buffer => { + return ed25519Sign(message, keyPair.getExtendPrivateKey()) +} diff --git a/dlt-connector/src/utils/derivationHelper.test.ts b/dlt-connector/src/utils/derivationHelper.test.ts new file mode 100644 index 000000000..0ad381aa8 --- /dev/null +++ b/dlt-connector/src/utils/derivationHelper.test.ts @@ -0,0 +1,15 @@ +import 'reflect-metadata' +import { Timestamp } from '../data/proto/3_3/Timestamp' +import { hardenDerivationIndex, HARDENED_KEY_BITMASK } from './derivationHelper' +import { timestampToDate } from './typeConverter' + +describe('utils', () => { + it('test bitmask for hardened keys', () => { + const derivationIndex = hardenDerivationIndex(1) + expect(derivationIndex).toBeGreaterThan(HARDENED_KEY_BITMASK) + }) + it('test TimestampToDate', () => { + const date = new Date('2011-04-17T12:01:10.109') + expect(timestampToDate(new Timestamp(date))).toEqual(date) + }) +}) diff --git a/dlt-connector/src/utils/derivationHelper.ts b/dlt-connector/src/utils/derivationHelper.ts new file mode 100644 index 000000000..0431ec339 --- /dev/null +++ b/dlt-connector/src/utils/derivationHelper.ts @@ -0,0 +1,17 @@ +export const HARDENED_KEY_BITMASK = 0x80000000 + +/* + * change derivation index from x => x' + * for more infos to hardened keys look here: + * https://en.bitcoin.it/wiki/BIP_0032 + */ +export const hardenDerivationIndex = (derivationIndex: number): number => { + /* + TypeScript uses signed integers by default, + but bip32-ed25519 expects an unsigned value for the derivation index. + The >>> shifts the bits 0 places to the right, which effectively makes no change to the value, + but forces TypeScript to treat derivationIndex as an unsigned value. + Source: ChatGPT + */ + return (derivationIndex | HARDENED_KEY_BITMASK) >>> 0 +} diff --git a/dlt-connector/src/utils/typeConverter.test.ts b/dlt-connector/src/utils/typeConverter.test.ts new file mode 100644 index 000000000..03863d0f2 --- /dev/null +++ b/dlt-connector/src/utils/typeConverter.test.ts @@ -0,0 +1,12 @@ +import 'reflect-metadata' +import { Timestamp } from '@/data/proto/3_3/Timestamp' +import { timestampToDate } from './typeConverter' + +describe('utils/typeConverter', () => { + it('timestampToDate', () => { + const now = new Date('Thu, 05 Oct 2023 11:55:18 +0000') + const timestamp = new Timestamp(now) + expect(timestamp.seconds).toBe(Math.round(now.getTime() / 1000)) + expect(timestampToDate(timestamp)).toEqual(now) + }) +}) diff --git a/dlt-connector/src/utils/typeConverter.ts b/dlt-connector/src/utils/typeConverter.ts index bd9e5f8da..15d4e8826 100644 --- a/dlt-connector/src/utils/typeConverter.ts +++ b/dlt-connector/src/utils/typeConverter.ts @@ -1,5 +1,12 @@ import { crypto_generichash as cryptoHash } from 'sodium-native' +import { Timestamp } from '@/data/proto/3_3/Timestamp' +import { TimestampSeconds } from '@/data/proto/3_3/TimestampSeconds' +import { TransactionBody } from '@/data/proto/3_3/TransactionBody' +import { logger } from '@/server/logger' +import { TransactionError } from '@/graphql/model/TransactionError' +import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType' + export const uuid4ToBuffer = (uuid: string): Buffer => { // Remove dashes from the UUIDv4 string const cleanedUUID = uuid.replace(/-/g, '') @@ -15,3 +22,41 @@ export const iotaTopicFromCommunityUUID = (communityUUID: string): string => { cryptoHash(hash, uuid4ToBuffer(communityUUID)) return hash.toString('hex') } + +export const timestampToDate = (timestamp: Timestamp): Date => { + let milliseconds = timestamp.nanoSeconds / 1000000 + milliseconds += timestamp.seconds * 1000 + return new Date(milliseconds) +} + +export const timestampSecondsToDate = (timestamp: TimestampSeconds): Date => { + return new Date(timestamp.seconds * 1000) +} + +export const base64ToBuffer = (base64: string): Buffer => { + return Buffer.from(base64, 'base64') +} + +export const bodyBytesToTransactionBody = (bodyBytes: Buffer): TransactionBody => { + try { + return TransactionBody.decode(new Uint8Array(bodyBytes)) + } catch (error) { + logger.error('error decoding body from gradido transaction: %s', error) + throw new TransactionError( + TransactionErrorType.PROTO_DECODE_ERROR, + 'cannot decode body from gradido transaction', + ) + } +} + +export const transactionBodyToBodyBytes = (transactionBody: TransactionBody): Buffer => { + try { + return Buffer.from(TransactionBody.encode(transactionBody).finish()) + } catch (error) { + logger.error('error encoding transaction body to body bytes', error) + throw new TransactionError( + TransactionErrorType.PROTO_ENCODE_ERROR, + 'cannot encode transaction body', + ) + } +} diff --git a/dlt-connector/tsconfig.json b/dlt-connector/tsconfig.json index 3abf9aead..e37b2a7a0 100644 --- a/dlt-connector/tsconfig.json +++ b/dlt-connector/tsconfig.json @@ -55,8 +55,7 @@ "@resolver/*": ["src/graphql/resolver/*"], "@scalar/*": ["src/graphql/scalar/*"], "@test/*": ["test/*"], - "@proto/*" : ["src/proto/*"], - "@controller/*": ["src/controller/*"], + "@proto/*" : ["src/proto/*"], "@validator/*" : ["src/graphql/validator/*"], "@typeorm/*" : ["src/typeorm/*"], /* external */ diff --git a/dlt-connector/yarn.lock b/dlt-connector/yarn.lock index 136e845f5..e54f2866d 100644 --- a/dlt-connector/yarn.lock +++ b/dlt-connector/yarn.lock @@ -20,7 +20,7 @@ resolved "https://registry.yarnpkg.com/@apollo/cache-control-types/-/cache-control-types-1.0.3.tgz#5da62cf64c3b4419dabfef4536b57a40c8ff0b47" integrity sha512-F17/vCp7QVwom9eG7ToauIKdAxpSoadsJnqIfyryLFSkLSOEqu+eC5Z3N8OXcUVStuOMcNHlyraRsA6rRICu4g== -"@apollo/protobufjs@1.2.7", "@apollo/protobufjs@^1.2.7": +"@apollo/protobufjs@1.2.7": version "1.2.7" resolved "https://registry.yarnpkg.com/@apollo/protobufjs/-/protobufjs-1.2.7.tgz#3a8675512817e4a046a897e5f4f16415f16a7d8a" integrity sha512-Lahx5zntHPZia35myYDBRuF58tlwPskwHc5CWBZC/4bMKB6siTBWwtMrkqXcsNwQiFSzSx5hKdRPUmemrEp3Gg== @@ -843,6 +843,11 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" +"@noble/hashes@^1.2.0": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.2.tgz#6f26dbc8fbc7205873ce3cee2f690eba0d421b39" + integrity sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ== + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -1125,6 +1130,13 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-20.7.0.tgz#c03de4572f114a940bc2ca909a33ddb2b925e470" integrity sha512-zI22/pJW2wUZOVyguFaUL1HABdmSVxpXrzIqkjsHmyUjNhPoWM1CKfvVuXfetHhIok4RY573cqS0mZ1SJEnoTg== +"@types/node@>=13.7.0": + version "20.8.7" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.8.7.tgz#ad23827850843de973096edfc5abc9e922492a25" + integrity sha512-21TKHHh3eUHIi2MloeptJWALuCu5H7HQTdTrWIFReA8ad+aggoX+lRes3ex7/FtpC+sVUpFMQ+QTfYr74mruiQ== + dependencies: + undici-types "~5.25.1" + "@types/node@^18.11.18": version "18.18.0" resolved "https://registry.yarnpkg.com/@types/node/-/node-18.18.0.tgz#bd19d5133a6e5e2d0152ec079ac27c120e7f1763" @@ -1628,6 +1640,22 @@ binary-extensions@^2.0.0: resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== +bip32-ed25519@^0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/bip32-ed25519/-/bip32-ed25519-0.0.4.tgz#218943e212c2d3152dfd6f3a929305e3fe86534c" + integrity sha512-KfazzGVLwl70WZ1r98dO+8yaJRTGgWHL9ITn4bXHQi2mB4cT3Hjh53tXWUpEWE1zKCln7PbyX8Z337VapAOb5w== + dependencies: + bn.js "^5.1.1" + elliptic "^6.4.1" + hash.js "^1.1.7" + +bip39@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/bip39/-/bip39-3.1.0.tgz#c55a418deaf48826a6ceb34ac55b3ee1577e18a3" + integrity sha512-c9kiwdk45Do5GL0vJMe7tS95VjCii65mYAH7DfWl3uW8AVzXKQVUm64i3hzVybBDMp9r7j9iNxR85+ul8MdN/A== + dependencies: + "@noble/hashes" "^1.2.0" + bl@^4.0.3: version "4.1.0" resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" @@ -1637,6 +1665,16 @@ bl@^4.0.3: inherits "^2.0.4" readable-stream "^3.4.0" +bn.js@^4.11.9: + version "4.12.0" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88" + integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA== + +bn.js@^5.1.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.1.tgz#0bc527a6a0d18d0aa8d5b0538ce4a77dccfa7b70" + integrity sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ== + body-parser@1.19.0: version "1.19.0" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" @@ -1711,6 +1749,11 @@ braces@^3.0.2, braces@~3.0.2: dependencies: fill-range "^7.0.1" +brorand@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" + integrity sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w== + browser-process-hrtime@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626" @@ -2343,6 +2386,19 @@ electron-to-chromium@^1.4.530: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.531.tgz#22966d894c4680726c17cf2908ee82ff5d26ac25" integrity sha512-H6gi5E41Rn3/mhKlPaT1aIMg/71hTAqn0gYEllSuw9igNWtvQwu185jiCZoZD29n7Zukgh7GVZ3zGf0XvkhqjQ== +elliptic@^6.4.1: + version "6.5.4" + resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.4.tgz#da37cebd31e79a1367e941b592ed1fbebd58abbb" + integrity sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ== + dependencies: + bn.js "^4.11.9" + brorand "^1.1.0" + hash.js "^1.0.0" + hmac-drbg "^1.0.1" + inherits "^2.0.4" + minimalistic-assert "^1.0.1" + minimalistic-crypto-utils "^1.0.1" + emittery@^0.8.1: version "0.8.1" resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.8.1.tgz#bb23cc86d03b30aa75a7f734819dee2e1ba70860" @@ -3338,11 +3394,28 @@ has@^1.0.3: dependencies: function-bind "^1.1.1" +hash.js@^1.0.0, hash.js@^1.0.3, hash.js@^1.1.7: + version "1.1.7" + resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42" + integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA== + dependencies: + inherits "^2.0.3" + minimalistic-assert "^1.0.1" + highlight.js@^10.7.1: version "10.7.3" resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.7.3.tgz#697272e3991356e40c3cac566a74eef681756531" integrity sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A== +hmac-drbg@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" + integrity sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg== + dependencies: + hash.js "^1.0.3" + minimalistic-assert "^1.0.0" + minimalistic-crypto-utils "^1.0.1" + html-encoding-sniffer@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz#42a6dc4fd33f00281176e8b23759ca4e4fa185f3" @@ -4379,6 +4452,11 @@ long@^4.0.0: resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA== +long@^5.0.0: + version "5.2.3" + resolved "https://registry.yarnpkg.com/long/-/long-5.2.3.tgz#a3ba97f3877cf1d778eccbcb048525ebb77499e1" + integrity sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q== + lower-case@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-2.0.2.tgz#6fa237c63dbdc4a82ca0fd882e4722dc5e634e28" @@ -4494,6 +4572,16 @@ mimic-response@^2.0.0: resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-2.1.0.tgz#d13763d35f613d09ec37ebb30bac0469c0ee8f43" integrity sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA== +minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" + integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== + +minimalistic-crypto-utils@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" + integrity sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg== + minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" @@ -5038,6 +5126,24 @@ prompts@^2.0.1: kleur "^3.0.3" sisteransi "^1.0.5" +protobufjs@^7.2.5: + version "7.2.5" + resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-7.2.5.tgz#45d5c57387a6d29a17aab6846dcc283f9b8e7f2d" + integrity sha512-gGXRSXvxQ7UiPgfw8gevrfRWcTlSbOFg+p/N+JVJEK5VhueL2miT6qTymqAmjr1Q5WbOCyJbyrk6JfWKwlFn6A== + dependencies: + "@protobufjs/aspromise" "^1.1.2" + "@protobufjs/base64" "^1.1.2" + "@protobufjs/codegen" "^2.0.4" + "@protobufjs/eventemitter" "^1.1.0" + "@protobufjs/fetch" "^1.1.0" + "@protobufjs/float" "^1.0.2" + "@protobufjs/inquire" "^1.1.0" + "@protobufjs/path" "^1.1.2" + "@protobufjs/pool" "^1.1.0" + "@protobufjs/utf8" "^1.1.0" + "@types/node" ">=13.7.0" + long "^5.0.0" + proxy-addr@~2.0.5, proxy-addr@~2.0.7: version "2.0.7" resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" @@ -6202,6 +6308,11 @@ undefsafe@^2.0.5: resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.5.tgz#38733b9327bdcd226db889fb723a6efd162e6e2c" integrity sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA== +undici-types@~5.25.1: + version "5.25.3" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.25.3.tgz#e044115914c85f0bcbb229f346ab739f064998c3" + integrity sha512-Ga1jfYwRn7+cP9v8auvEXN1rX3sWqlayd4HP7OKk4mZWylEmu3KzXDUGrQUN6Ol7qo1gPvB2e5gX6udnyEPgdA== + universalify@^0.1.0: version "0.1.2" resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" diff --git a/dlt-database/entity/0001-init_db/Account.ts b/dlt-database/entity/0001-init_db/Account.ts index 43910122a..d6d4a3095 100644 --- a/dlt-database/entity/0001-init_db/Account.ts +++ b/dlt-database/entity/0001-init_db/Account.ts @@ -8,7 +8,7 @@ import { BaseEntity, } from 'typeorm' import { User } from '../User' -import { TransactionRecipe } from '../TransactionRecipe' +import { TransactionRecipe } from './TransactionRecipe' import { ConfirmedTransaction } from '../ConfirmedTransaction' import { DecimalTransformer } from '../../src/typeorm/DecimalTransformer' import { Decimal } from 'decimal.js-light' diff --git a/dlt-database/entity/0001-init_db/Community.ts b/dlt-database/entity/0001-init_db/Community.ts index 96ddd59d6..52f8c1155 100644 --- a/dlt-database/entity/0001-init_db/Community.ts +++ b/dlt-database/entity/0001-init_db/Community.ts @@ -8,7 +8,7 @@ import { BaseEntity, } from 'typeorm' import { Account } from '../Account' -import { TransactionRecipe } from '../TransactionRecipe' +import { TransactionRecipe } from './TransactionRecipe' import { AccountCommunity } from '../AccountCommunity' @Entity('communities') diff --git a/dlt-database/entity/0001-init_db/ConfirmedTransaction.ts b/dlt-database/entity/0001-init_db/ConfirmedTransaction.ts index 16786a713..a5d22dc80 100644 --- a/dlt-database/entity/0001-init_db/ConfirmedTransaction.ts +++ b/dlt-database/entity/0001-init_db/ConfirmedTransaction.ts @@ -10,7 +10,7 @@ import { import { Decimal } from 'decimal.js-light' import { DecimalTransformer } from '../../src/typeorm/DecimalTransformer' -import { Account } from '../Account' +import { Account } from './Account' import { TransactionRecipe } from '../TransactionRecipe' @Entity('confirmed_transactions') diff --git a/dlt-database/entity/0001-init_db/TransactionRecipe.ts b/dlt-database/entity/0001-init_db/TransactionRecipe.ts index 934e81d02..db311e64f 100644 --- a/dlt-database/entity/0001-init_db/TransactionRecipe.ts +++ b/dlt-database/entity/0001-init_db/TransactionRecipe.ts @@ -10,8 +10,8 @@ import { import { Decimal } from 'decimal.js-light' import { DecimalTransformer } from '../../src/typeorm/DecimalTransformer' -import { Account } from '../Account' -import { Community } from '../Community' +import { Account } from './Account' +import { Community } from './Community' import { ConfirmedTransaction } from '../ConfirmedTransaction' @Entity('transaction_recipes') diff --git a/dlt-database/entity/0002-refactor_add_community/ConfirmedTransaction.ts b/dlt-database/entity/0002-refactor_add_community/ConfirmedTransaction.ts index 5d2a38f65..1b8df403b 100644 --- a/dlt-database/entity/0002-refactor_add_community/ConfirmedTransaction.ts +++ b/dlt-database/entity/0002-refactor_add_community/ConfirmedTransaction.ts @@ -10,7 +10,7 @@ import { import { Decimal } from 'decimal.js-light' import { DecimalTransformer } from '../../src/typeorm/DecimalTransformer' -import { Account } from '../Account' +import { Account } from './Account' import { TransactionRecipe } from '../TransactionRecipe' @Entity('confirmed_transactions') diff --git a/dlt-database/entity/0003-refactor_transaction_recipe/Account.ts b/dlt-database/entity/0003-refactor_transaction_recipe/Account.ts new file mode 100644 index 000000000..067a140eb --- /dev/null +++ b/dlt-database/entity/0003-refactor_transaction_recipe/Account.ts @@ -0,0 +1,92 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + ManyToOne, + JoinColumn, + OneToMany, + BaseEntity, +} from 'typeorm' +import { User } from '../User' +import { Transaction } from '../Transaction' +import { DecimalTransformer } from '../../src/typeorm/DecimalTransformer' +import { Decimal } from 'decimal.js-light' +import { AccountCommunity } from '../AccountCommunity' + +@Entity('accounts') +export class Account extends BaseEntity { + @PrimaryGeneratedColumn('increment', { unsigned: true }) + id: number + + @ManyToOne(() => User, (user) => user.accounts, { cascade: true, eager: true }) // Assuming you have a User entity with 'accounts' relation + @JoinColumn({ name: 'user_id' }) + user?: User + + // if user id is null, account belongs to community gmw or auf + @Column({ name: 'user_id', type: 'int', unsigned: true, nullable: true }) + userId?: number + + @Column({ name: 'derivation_index', type: 'int', unsigned: true, nullable: true }) + derivationIndex?: number + + @Column({ name: 'derive2_pubkey', type: 'binary', length: 32, unique: true }) + derive2Pubkey: Buffer + + @Column({ type: 'tinyint', unsigned: true }) + type: number + + @Column({ + name: 'created_at', + type: 'datetime', + precision: 3, + default: () => 'CURRENT_TIMESTAMP(3)', + }) + createdAt: Date + + @Column({ name: 'confirmed_at', type: 'datetime', nullable: true }) + confirmedAt?: Date + + @Column({ + type: 'decimal', + precision: 40, + scale: 20, + default: 0, + transformer: DecimalTransformer, + }) + balance: Decimal + + @Column({ + name: 'balance_date', + type: 'datetime', + default: () => 'CURRENT_TIMESTAMP()', + }) + balanceDate: Date + + @Column({ + name: 'balance_created_at', + type: 'decimal', + precision: 40, + scale: 20, + default: 0, + transformer: DecimalTransformer, + }) + balanceCreatedAt: Decimal + + @Column({ + name: 'balance_created_at_date', + type: 'datetime', + precision: 3, + default: () => 'CURRENT_TIMESTAMP(3)', + }) + balanceCreatedAtDate: Date + + @OneToMany(() => AccountCommunity, (accountCommunity) => accountCommunity.account) + @JoinColumn({ name: 'account_id' }) + accountCommunities: AccountCommunity[] + + @OneToMany(() => Transaction, (transaction) => transaction.signingAccount) + transactionSigning?: Transaction[] + + @OneToMany(() => Transaction, (transaction) => transaction.recipientAccount) + transactionRecipient?: Transaction[] +} diff --git a/dlt-database/entity/0003-refactor_transaction_recipe/Community.ts b/dlt-database/entity/0003-refactor_transaction_recipe/Community.ts new file mode 100644 index 000000000..ade3afc90 --- /dev/null +++ b/dlt-database/entity/0003-refactor_transaction_recipe/Community.ts @@ -0,0 +1,68 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + JoinColumn, + OneToOne, + OneToMany, + BaseEntity, +} from 'typeorm' +import { Account } from '../Account' +import { Transaction } from '../Transaction' +import { AccountCommunity } from '../AccountCommunity' + +@Entity('communities') +export class Community extends BaseEntity { + @PrimaryGeneratedColumn('increment', { unsigned: true }) + id: number + + @Column({ name: 'iota_topic', collation: 'utf8mb4_unicode_ci', unique: true }) + iotaTopic: string + + @Column({ name: 'root_pubkey', type: 'binary', length: 32, unique: true, nullable: true }) + rootPubkey?: Buffer + + @Column({ name: 'root_privkey', type: 'binary', length: 64, nullable: true }) + rootPrivkey?: Buffer + + @Column({ name: 'root_chaincode', type: 'binary', length: 32, nullable: true }) + rootChaincode?: Buffer + + @Column({ type: 'tinyint', default: true }) + foreign: boolean + + @Column({ name: 'gmw_account_id', type: 'int', unsigned: true, nullable: true }) + gmwAccountId?: number + + @OneToOne(() => Account, { cascade: true }) + @JoinColumn({ name: 'gmw_account_id' }) + gmwAccount?: Account + + @Column({ name: 'auf_account_id', type: 'int', unsigned: true, nullable: true }) + aufAccountId?: number + + @OneToOne(() => Account, { cascade: true }) + @JoinColumn({ name: 'auf_account_id' }) + aufAccount?: Account + + @Column({ + name: 'created_at', + type: 'datetime', + precision: 3, + default: () => 'CURRENT_TIMESTAMP(3)', + }) + createdAt: Date + + @Column({ name: 'confirmed_at', type: 'datetime', nullable: true }) + confirmedAt?: Date + + @OneToMany(() => AccountCommunity, (accountCommunity) => accountCommunity.community) + @JoinColumn({ name: 'community_id' }) + accountCommunities: AccountCommunity[] + + @OneToMany(() => Transaction, (recipe) => recipe.senderCommunity) + transactionSender?: Transaction[] + + @OneToMany(() => Transaction, (recipe) => recipe.recipientCommunity) + transactionRecipient?: Transaction[] +} diff --git a/dlt-database/entity/0003-refactor_transaction_recipe/Transaction.ts b/dlt-database/entity/0003-refactor_transaction_recipe/Transaction.ts new file mode 100644 index 000000000..ac48f07b6 --- /dev/null +++ b/dlt-database/entity/0003-refactor_transaction_recipe/Transaction.ts @@ -0,0 +1,120 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + ManyToOne, + OneToOne, + JoinColumn, + BaseEntity, +} from 'typeorm' +import { Decimal } from 'decimal.js-light' + +import { DecimalTransformer } from '../../src/typeorm/DecimalTransformer' +import { Account } from '../Account' +import { Community } from '../Community' + +@Entity('transactions') +export class Transaction extends BaseEntity { + @PrimaryGeneratedColumn('increment', { unsigned: true, type: 'bigint' }) + id: number + + @Column({ name: 'iota_message_id', type: 'binary', length: 32, nullable: true }) + iotaMessageId?: Buffer + + @Column({ name: 'backend_transaction_id', type: 'bigint', unsigned: true, nullable: true }) + backendTransactionId?: number + + @OneToOne(() => Transaction) + // eslint-disable-next-line no-use-before-define + paringTransaction?: Transaction + + @Column({ name: 'paring_transaction_id', type: 'bigint', unsigned: true, nullable: true }) + paringTransactionId?: number + + // if transaction has a sender than it is also the sender account + @ManyToOne(() => Account, (account) => account.transactionSigning) + @JoinColumn({ name: 'signing_account_id' }) + signingAccount?: Account + + @Column({ name: 'signing_account_id', type: 'int', unsigned: true, nullable: true }) + signingAccountId?: number + + @ManyToOne(() => Account, (account) => account.transactionRecipient) + @JoinColumn({ name: 'recipient_account_id' }) + recipientAccount?: Account + + @Column({ name: 'recipient_account_id', type: 'int', unsigned: true, nullable: true }) + recipientAccountId?: number + + @ManyToOne(() => Community, (community) => community.transactionSender, { + eager: true, + }) + @JoinColumn({ name: 'sender_community_id' }) + senderCommunity: Community + + @Column({ name: 'sender_community_id', type: 'int', unsigned: true }) + senderCommunityId: number + + @ManyToOne(() => Community, (community) => community.transactionRecipient) + @JoinColumn({ name: 'recipient_community_id' }) + recipientCommunity?: Community + + @Column({ name: 'recipient_community_id', type: 'int', unsigned: true, nullable: true }) + recipientCommunityId?: number + + @Column({ + type: 'decimal', + precision: 40, + scale: 20, + nullable: true, + transformer: DecimalTransformer, + }) + amount?: Decimal + + @Column({ + name: 'account_balance_created_at', + type: 'decimal', + precision: 40, + scale: 20, + transformer: DecimalTransformer, + }) + accountBalanceCreatedAt: Decimal + + @Column({ type: 'tinyint' }) + type: number + + @Column({ name: 'created_at', type: 'datetime', precision: 3 }) + createdAt: Date + + @Column({ name: 'body_bytes', type: 'blob' }) + bodyBytes: Buffer + + @Column({ type: 'binary', length: 64, unique: true }) + signature: Buffer + + @Column({ name: 'protocol_version', type: 'varchar', length: 255, default: '1' }) + protocolVersion: string + + @Column({ type: 'bigint' }) + nr: number + + @Column({ name: 'running_hash', type: 'binary', length: 48 }) + runningHash: Buffer + + @Column({ + name: 'account_balance', + type: 'decimal', + precision: 40, + scale: 20, + nullable: false, + default: 0, + transformer: DecimalTransformer, + }) + accountBalanceConfirmedAt: Decimal + + @Column({ name: 'iota_milestone', type: 'bigint', nullable: true }) + iotaMilestone?: number + + @Column({ name: 'confirmed_at', type: 'datetime' }) + confirmedAt: Date +} diff --git a/dlt-database/entity/Account.ts b/dlt-database/entity/Account.ts index ed1e92840..3d7713ba9 100644 --- a/dlt-database/entity/Account.ts +++ b/dlt-database/entity/Account.ts @@ -1 +1 @@ -export { Account } from './0002-refactor_add_community/Account' +export { Account } from './0003-refactor_transaction_recipe/Account' diff --git a/dlt-database/entity/Community.ts b/dlt-database/entity/Community.ts index 211837e40..cb4d34c43 100644 --- a/dlt-database/entity/Community.ts +++ b/dlt-database/entity/Community.ts @@ -1 +1 @@ -export { Community } from './0002-refactor_add_community/Community' +export { Community } from './0003-refactor_transaction_recipe/Community' diff --git a/dlt-database/entity/Transaction.ts b/dlt-database/entity/Transaction.ts new file mode 100644 index 000000000..113eb3450 --- /dev/null +++ b/dlt-database/entity/Transaction.ts @@ -0,0 +1 @@ +export { Transaction } from './0003-refactor_transaction_recipe/Transaction' diff --git a/dlt-database/entity/index.ts b/dlt-database/entity/index.ts index 74c2e2258..ba7ea2663 100644 --- a/dlt-database/entity/index.ts +++ b/dlt-database/entity/index.ts @@ -1,19 +1,17 @@ import { Account } from './Account' import { AccountCommunity } from './AccountCommunity' import { Community } from './Community' -import { ConfirmedTransaction } from './ConfirmedTransaction' import { InvalidTransaction } from './InvalidTransaction' import { Migration } from './Migration' -import { TransactionRecipe } from './TransactionRecipe' +import { Transaction } from './Transaction' import { User } from './User' export const entities = [ AccountCommunity, Account, Community, - ConfirmedTransaction, InvalidTransaction, Migration, - TransactionRecipe, + Transaction, User, ] diff --git a/dlt-database/migrations/0003-refactor_transaction_recipe.ts b/dlt-database/migrations/0003-refactor_transaction_recipe.ts new file mode 100644 index 000000000..577b24938 --- /dev/null +++ b/dlt-database/migrations/0003-refactor_transaction_recipe.ts @@ -0,0 +1,97 @@ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +export async function upgrade(queryFn: (query: string, values?: any[]) => Promise>) { + // write upgrade logic as parameter of queryFn + await queryFn(`DROP TABLE \`transaction_recipes\`;`) + await queryFn(`DROP TABLE \`confirmed_transactions\`;`) + + await queryFn( + `ALTER TABLE \`accounts\` MODIFY COLUMN \`derivation_index\` int(10) unsigned NULL DEFAULT NULL;`, + ) + await queryFn( + `ALTER TABLE \`accounts\` ADD COLUMN \`account_balance_created_at\` decimal(40,20) NOT NULL DEFAULT 0 AFTER \`balance_date\`;`, + ) + await queryFn( + `ALTER TABLE \`accounts\` ADD COLUMN \`balance_created_at_date\` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) AFTER \`account_balance_created_at\`;`, + ) + + await queryFn( + `CREATE TABLE \`transactions\` ( + \`id\` bigint unsigned NOT NULL AUTO_INCREMENT, + \`iota_message_id\` varbinary(32) DEFAULT NULL, + \`backend_transaction_id\` bigint unsigned DEFAULT NULL, + \`paring_transaction_id\` bigint unsigned DEFAULT NULL, + \`signing_account_id\` int unsigned DEFAULT NULL, + \`recipient_account_id\` int unsigned DEFAULT NULL, + \`sender_community_id\` int unsigned NOT NULL, + \`recipient_community_id\` int unsigned DEFAULT NULL, + \`amount\` decimal(40, 20) DEFAULT NULL, + \`account_balance_created_at\` decimal(40, 20) NOT NULL, + \`type\` tinyint NOT NULL, + \`created_at\` datetime(3) NOT NULL, + \`body_bytes\` blob NOT NULL, + \`signature\` varbinary(64) NOT NULL, + \`protocol_version\` varchar(255) NOT NULL DEFAULT '1', + \`nr\` bigint NOT NULL, + \`running_hash\` varbinary(48) NOT NULL, + \`account_balance\` decimal(40, 20) NOT NULL DEFAULT 0.00000000000000000000, + \`iota_milestone\` bigint DEFAULT NULL, + \`confirmed_at\` datetime NOT NULL, + PRIMARY KEY (\`id\`), + UNIQUE KEY \`signature\` (\`signature\`), + FOREIGN KEY (\`signing_account_id\`) REFERENCES accounts(id), + FOREIGN KEY (\`recipient_account_id\`) REFERENCES accounts(id), + FOREIGN KEY (\`sender_community_id\`) REFERENCES communities(id), + FOREIGN KEY (\`recipient_community_id\`) REFERENCES communities(id) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + `, + ) + + await queryFn(`ALTER TABLE \`communities\` ADD UNIQUE(\`iota_topic\`);`) +} + +export async function downgrade(queryFn: (query: string, values?: any[]) => Promise>) { + await queryFn(` + CREATE TABLE IF NOT EXISTS \`transaction_recipes\` ( + \`id\` bigint unsigned NOT NULL AUTO_INCREMENT, + \`iota_message_id\` binary(32) DEFAULT NULL, + \`signing_account_id\` int(10) unsigned NOT NULL, + \`recipient_account_id\` int(10) unsigned DEFAULT NULL, + \`sender_community_id\` int(10) unsigned NOT NULL, + \`recipient_community_id\` int(10) unsigned DEFAULT NULL, + \`amount\` decimal(40,20) DEFAULT NULL, + \`type\` tinyint unsigned NOT NULL, + \`created_at\` datetime(3) NOT NULL, + \`body_bytes\` BLOB NOT NULL, + \`signature\` binary(64) NOT NULL, + \`protocol_version\` int(10) NOT NULL DEFAULT 1, + PRIMARY KEY (\`id\`), + FOREIGN KEY (\`signing_account_id\`) REFERENCES accounts(id), + FOREIGN KEY (\`recipient_account_id\`) REFERENCES accounts(id), + FOREIGN KEY (\`sender_community_id\`) REFERENCES communities(id), + FOREIGN KEY (\`recipient_community_id\`) REFERENCES communities(id) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;`) + + await queryFn(` + CREATE TABLE IF NOT EXISTS \`confirmed_transactions\` ( + \`id\` bigint unsigned NOT NULL AUTO_INCREMENT, + \`transaction_recipe_id\` bigint unsigned NOT NULL, + \`nr\` bigint unsigned NOT NULL, + \`running_hash\` binary(48) NOT NULL, + \`account_id\` int(10) unsigned NOT NULL, + \`account_balance\` decimal(40,20) NOT NULL DEFAULT 0, + \`iota_milestone\` bigint NOT NULL, + \`confirmed_at\` datetime NOT NULL, + PRIMARY KEY (\`id\`), + FOREIGN KEY (\`transaction_recipe_id\`) REFERENCES transaction_recipes(id), + FOREIGN KEY (\`account_id\`) REFERENCES accounts(id) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;`) + + await queryFn( + `ALTER TABLE \`accounts\` MODIFY COLUMN \`derivation_index\` int(10) unsigned NOT NULL;`, + ) + await queryFn(`ALTER TABLE \`accounts\` DROP COLUMN \`account_balance_created_at\`;`) + await queryFn(`ALTER TABLE \`accounts\` DROP COLUMN \`balance_created_at_date\`;`) + await queryFn(`DROP TABLE \`transactions\`;`) +}