cleanup not longer needed code

This commit is contained in:
einhornimmond 2024-09-22 17:01:59 +02:00
parent 2727b6ebe9
commit a826eaf838
55 changed files with 65 additions and 2862 deletions

View File

@ -1,65 +0,0 @@
/* eslint-disable camelcase */
import { Account } from '@entity/Account'
import Decimal from 'decimal.js-light'
import {
AddressType,
AddressType_COMMUNITY_AUF,
AddressType_COMMUNITY_GMW,
} from 'gradido-blockchain-js'
import { KeyPair } from '@/data/KeyPair'
import { UserAccountDraft } from '@/graphql/input/UserAccountDraft'
import { hardenDerivationIndex } from '@/utils/derivationHelper'
import { accountTypeToAddressType } from '@/utils/typeConverter'
const GMW_ACCOUNT_DERIVATION_INDEX = 1
const AUF_ACCOUNT_DERIVATION_INDEX = 2
export class AccountFactory {
public static createAccount(
createdAt: Date,
derivationIndex: number,
type: AddressType,
parentKeyPair: KeyPair,
): Account {
const account = Account.create()
account.derivationIndex = derivationIndex
account.derive2Pubkey = parentKeyPair.derive([derivationIndex]).publicKey
account.type = type.valueOf()
account.createdAt = createdAt
account.balanceOnConfirmation = new Decimal(0)
account.balanceOnCreation = new Decimal(0)
account.balanceCreatedAt = createdAt
return account
}
public static createAccountFromUserAccountDraft(
{ createdAt, accountType, user }: UserAccountDraft,
parentKeyPair: KeyPair,
): Account {
return AccountFactory.createAccount(
new Date(createdAt),
user.accountNr ?? 1,
accountTypeToAddressType(accountType),
parentKeyPair,
)
}
public static createGmwAccount(keyPair: KeyPair, createdAt: Date): Account {
return AccountFactory.createAccount(
createdAt,
hardenDerivationIndex(GMW_ACCOUNT_DERIVATION_INDEX),
AddressType_COMMUNITY_GMW,
keyPair,
)
}
public static createAufAccount(keyPair: KeyPair, createdAt: Date): Account {
return AccountFactory.createAccount(
createdAt,
hardenDerivationIndex(AUF_ACCOUNT_DERIVATION_INDEX),
AddressType_COMMUNITY_AUF,
keyPair,
)
}
}

View File

@ -1,35 +0,0 @@
import { Account } from '@entity/Account'
import { LogError } from '@/server/LogError'
import { KeyPair } from './KeyPair'
import { UserLogic } from './User.logic'
export class AccountLogic {
// eslint-disable-next-line no-useless-constructor
public constructor(private self: Account) {}
/**
* calculate account key pair starting from community key pair => derive user key pair => derive account key pair
* @param communityKeyPair
*/
public calculateKeyPair(communityKeyPair: KeyPair): KeyPair {
if (!this.self.user) {
throw new LogError('missing user')
}
const userLogic = new UserLogic(this.self.user)
const accountKeyPair = userLogic
.calculateKeyPair(communityKeyPair)
.derive([this.self.derivationIndex])
if (
this.self.derive2Pubkey &&
this.self.derive2Pubkey.compare(accountKeyPair.publicKey) !== 0
) {
throw new LogError(
'The freshly derived public key does not correspond to the stored public key',
)
}
return accountKeyPair
}
}

View File

@ -1,34 +0,0 @@
import { Account } from '@entity/Account'
import { User } from '@entity/User'
import { In } from 'typeorm'
import { UserIdentifier } from '@/graphql/input/UserIdentifier'
import { getDataSource } from '@/typeorm/DataSource'
export const AccountRepository = getDataSource()
.getRepository(Account)
.extend({
findAccountsByPublicKeys(publicKeys: Buffer[]): Promise<Account[]> {
return this.findBy({ derive2Pubkey: In(publicKeys) })
},
async findAccountByPublicKey(publicKey: Buffer | undefined): Promise<Account | undefined> {
if (!publicKey) return undefined
return (await this.findOneBy({ derive2Pubkey: Buffer.from(publicKey) })) ?? undefined
},
async findAccountByUserIdentifier({
uuid,
accountNr,
}: UserIdentifier): Promise<Account | undefined> {
const user = await User.findOne({
where: { gradidoID: uuid, accounts: { derivationIndex: accountNr ?? 1 } },
relations: { accounts: true },
})
if (user && user.accounts?.length === 1) {
const account = user.accounts[0]
account.user = user
return account
}
},
})

View File

@ -1,203 +0,0 @@
/* eslint-disable camelcase */
import 'reflect-metadata'
import { Decimal } from 'decimal.js-light'
import { TestDB } from '@test/TestDB'
import {
AddressType_COMMUNITY_AUF,
AddressType_COMMUNITY_GMW,
AddressType_COMMUNITY_HUMAN,
} from 'gradido-blockchain-js'
import { AccountType } from '@/graphql/enum/AccountType'
import { UserAccountDraft } from '@/graphql/input/UserAccountDraft'
import { UserIdentifier } from '@/graphql/input/UserIdentifier'
import { AccountFactory } from './Account.factory'
import { AccountRepository } from './Account.repository'
import { KeyPair } from './KeyPair'
import { Mnemonic } from './Mnemonic'
import { UserFactory } from './User.factory'
import { UserLogic } from './User.logic'
const con = TestDB.instance
jest.mock('@typeorm/DataSource', () => ({
getDataSource: jest.fn(() => TestDB.instance.dbConnect),
}))
describe('data/Account test factory and repository', () => {
const now = new Date()
const keyPair1 = new KeyPair(new Mnemonic('62ef251edc2416f162cd24ab1711982b'))
const keyPair2 = new KeyPair(new Mnemonic('000a0000000002000000000003000070'))
const keyPair3 = new KeyPair(new Mnemonic('00ba541a1000020000000000300bda70'))
const userGradidoID = '6be949ab-8198-4acf-ba63-740089081d61'
describe('test factory methods', () => {
beforeAll(async () => {
await con.setupTestDB()
})
afterAll(async () => {
await con.teardownTestDB()
})
it('test createAccount', () => {
const account = AccountFactory.createAccount(now, 1, AddressType_COMMUNITY_HUMAN, keyPair1)
expect(account).toMatchObject({
derivationIndex: 1,
derive2Pubkey: Buffer.from(
'cb88043ef4833afc01d6ed9b34e1aa48e79dce5ff97c07090c6600ec05f6d994',
'hex',
),
type: AddressType_COMMUNITY_HUMAN,
createdAt: now,
balanceCreatedAt: now,
balanceOnConfirmation: new Decimal(0),
balanceOnCreation: new Decimal(0),
})
})
it('test createAccountFromUserAccountDraft', () => {
const userAccountDraft = new UserAccountDraft()
userAccountDraft.createdAt = now.toISOString()
userAccountDraft.accountType = AccountType.COMMUNITY_HUMAN
userAccountDraft.user = new UserIdentifier()
userAccountDraft.user.accountNr = 1
const account = AccountFactory.createAccountFromUserAccountDraft(userAccountDraft, keyPair1)
expect(account).toMatchObject({
derivationIndex: 1,
derive2Pubkey: Buffer.from(
'cb88043ef4833afc01d6ed9b34e1aa48e79dce5ff97c07090c6600ec05f6d994',
'hex',
),
type: AddressType_COMMUNITY_HUMAN,
createdAt: now,
balanceCreatedAt: now,
balanceOnConfirmation: new Decimal(0),
balanceOnCreation: new Decimal(0),
})
})
it('test createGmwAccount', () => {
const account = AccountFactory.createGmwAccount(keyPair1, now)
expect(account).toMatchObject({
derivationIndex: 2147483649,
derive2Pubkey: Buffer.from(
'05f0060357bb73bd290283870fc47a10b3764f02ca26938479ed853f46145366',
'hex',
),
type: AddressType_COMMUNITY_GMW,
createdAt: now,
balanceCreatedAt: now,
balanceOnConfirmation: new Decimal(0),
balanceOnCreation: new Decimal(0),
})
})
it('test createAufAccount', () => {
const account = AccountFactory.createAufAccount(keyPair1, now)
expect(account).toMatchObject({
derivationIndex: 2147483650,
derive2Pubkey: Buffer.from(
'6c749f8693a4a58c948e5ae54df11e2db33d2f98673b56e0cf19c0132614ab59',
'hex',
),
type: AddressType_COMMUNITY_AUF,
createdAt: now,
balanceCreatedAt: now,
balanceOnConfirmation: new Decimal(0),
balanceOnCreation: new Decimal(0),
})
})
})
describe('test repository functions', () => {
beforeAll(async () => {
await con.setupTestDB()
await Promise.all([
AccountFactory.createAufAccount(keyPair1, now).save(),
AccountFactory.createGmwAccount(keyPair1, now).save(),
AccountFactory.createAufAccount(keyPair2, now).save(),
AccountFactory.createGmwAccount(keyPair2, now).save(),
AccountFactory.createAufAccount(keyPair3, now).save(),
AccountFactory.createGmwAccount(keyPair3, now).save(),
])
const userAccountDraft = new UserAccountDraft()
userAccountDraft.accountType = AccountType.COMMUNITY_HUMAN
userAccountDraft.createdAt = now.toString()
userAccountDraft.user = new UserIdentifier()
userAccountDraft.user.accountNr = 1
userAccountDraft.user.uuid = userGradidoID
const user = UserFactory.create(userAccountDraft, keyPair1)
const userLogic = new UserLogic(user)
const account = AccountFactory.createAccountFromUserAccountDraft(
userAccountDraft,
userLogic.calculateKeyPair(keyPair1),
)
account.user = user
// user is set to cascade: ['insert'] will be saved together with account
await account.save()
})
afterAll(async () => {
await con.teardownTestDB()
})
it('test findAccountsByPublicKeys', async () => {
const accounts = await AccountRepository.findAccountsByPublicKeys([
Buffer.from('6c749f8693a4a58c948e5ae54df11e2db33d2f98673b56e0cf19c0132614ab59', 'hex'),
Buffer.from('0fa996b73b624592fe326b8500cb1e3f10026112b374d84c87d097f4d489c019', 'hex'),
Buffer.from('0ffa996b73b624592f26b850b0cb1e3f1026112b374d84c87d017f4d489c0197', 'hex'), // invalid
])
expect(accounts).toHaveLength(2)
expect(accounts).toMatchObject(
expect.arrayContaining([
expect.objectContaining({
derivationIndex: 2147483649,
derive2Pubkey: Buffer.from(
'0fa996b73b624592fe326b8500cb1e3f10026112b374d84c87d097f4d489c019',
'hex',
),
type: AddressType_COMMUNITY_GMW,
}),
expect.objectContaining({
derivationIndex: 2147483650,
derive2Pubkey: Buffer.from(
'6c749f8693a4a58c948e5ae54df11e2db33d2f98673b56e0cf19c0132614ab59',
'hex',
),
type: AddressType_COMMUNITY_AUF,
}),
]),
)
})
it('test findAccountByPublicKey', async () => {
expect(
await AccountRepository.findAccountByPublicKey(
Buffer.from('6c749f8693a4a58c948e5ae54df11e2db33d2f98673b56e0cf19c0132614ab59', 'hex'),
),
).toMatchObject({
derivationIndex: 2147483650,
derive2Pubkey: Buffer.from(
'6c749f8693a4a58c948e5ae54df11e2db33d2f98673b56e0cf19c0132614ab59',
'hex',
),
type: AddressType_COMMUNITY_AUF,
})
})
it('test findAccountByUserIdentifier', async () => {
const userIdentifier = new UserIdentifier()
userIdentifier.accountNr = 1
userIdentifier.uuid = userGradidoID
expect(await AccountRepository.findAccountByUserIdentifier(userIdentifier)).toMatchObject({
derivationIndex: 1,
derive2Pubkey: Buffer.from(
'2099c004a26e5387c9fbbc9bb0f552a9642d3fd7c710ae5802b775d24ff36f93',
'hex',
),
type: AddressType_COMMUNITY_HUMAN,
})
})
})
})

View File

@ -1,82 +0,0 @@
import { Community } from '@entity/Community'
import { FindOptionsSelect, In, IsNull, Not } from 'typeorm'
import { CommunityArg } from '@/graphql/arg/CommunityArg'
import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType'
import { CommunityDraft } from '@/graphql/input/CommunityDraft'
import { UserIdentifier } from '@/graphql/input/UserIdentifier'
import { TransactionError } from '@/graphql/model/TransactionError'
import { LogError } from '@/server/LogError'
import { getDataSource } from '@/typeorm/DataSource'
import { uuid4ToHash } from '@/utils/typeConverter'
import { KeyPair } from './KeyPair'
export const CommunityRepository = getDataSource()
.getRepository(Community)
.extend({
async isExist(community: CommunityDraft | string): Promise<boolean> {
const iotaTopic =
community instanceof CommunityDraft ? uuid4ToHash(community.uuid) : community
const result = await this.find({
where: { iotaTopic },
})
return result.length > 0
},
async findByCommunityArg({ uuid, foreign, confirmed }: CommunityArg): Promise<Community[]> {
return await this.find({
where: {
...(uuid && { iotaTopic: uuid4ToHash(uuid) }),
...(foreign && { foreign }),
...(confirmed && { confirmedAt: Not(IsNull()) }),
},
})
},
async findByCommunityUuid(communityUuid: string): Promise<Community | null> {
return await this.findOneBy({ iotaTopic: uuid4ToHash(communityUuid) })
},
async findByIotaTopic(iotaTopic: string): Promise<Community | null> {
return await this.findOneBy({ iotaTopic })
},
findCommunitiesByTopics(topics: string[]): Promise<Community[]> {
return this.findBy({ iotaTopic: In(topics) })
},
async getCommunityForUserIdentifier(
identifier: UserIdentifier,
): Promise<Community | undefined> {
if (!identifier.communityUuid) {
throw new TransactionError(TransactionErrorType.MISSING_PARAMETER, 'community uuid not set')
}
return (
(await this.findOneBy({
iotaTopic: uuid4ToHash(identifier.communityUuid),
})) ?? undefined
)
},
findAll(select: FindOptionsSelect<Community>): Promise<Community[]> {
return this.find({ select })
},
async loadHomeCommunityKeyPair(): Promise<KeyPair> {
const community = await this.findOneOrFail({
where: { foreign: false },
select: { rootChaincode: true, rootPubkey: true, rootEncryptedPrivkey: true },
})
if (!community.rootChaincode || !community.rootEncryptedPrivkey) {
throw new LogError('Missing chaincode or private key for home community')
}
return new KeyPair(community)
},
async loadHomeCommunity(): Promise<Community> {
return await this.findOneOrFail({
where: { foreign: false },
})
},
})

View File

@ -1,107 +0,0 @@
import { Community } from '@entity/Community'
// https://www.npmjs.com/package/bip32-ed25519
import {
KeyPairEd25519,
MemoryBlock,
Passphrase,
SecretKeyCryptography,
SignaturePair,
} from 'gradido-blockchain-js'
import { CONFIG } from '@/config'
import { LogError } from '@/server/LogError'
/**
* Class Managing Key Pair and also generate, sign and verify signature with it
*/
export class KeyPair {
private _ed25519KeyPair: KeyPairEd25519
/**
* @param input: KeyPairEd25519 = already loaded KeyPairEd25519
* @param input: Passphrase = Passphrase which work as seed for generating algorithms
* @param input: MemoryBlock = a seed at least 32 byte
* @param input: Community = community entity with keys loaded from db
*/
public constructor(input: KeyPairEd25519 | Passphrase | MemoryBlock | Community) {
let keyPair: KeyPairEd25519 | null = null
if (input instanceof KeyPairEd25519) {
keyPair = input
} else if (input instanceof Passphrase) {
keyPair = KeyPairEd25519.create(input)
} else if (input instanceof MemoryBlock) {
keyPair = KeyPairEd25519.create(input)
} else if (input instanceof Community) {
if (!input.rootEncryptedPrivkey || !input.rootChaincode || !input.rootPubkey) {
throw new LogError(
'missing encrypted private key or chaincode or public key in commmunity entity',
)
}
const secretBox = this.createSecretBox(input.iotaTopic)
keyPair = new KeyPairEd25519(
new MemoryBlock(input.rootPubkey),
secretBox.decrypt(new MemoryBlock(input.rootEncryptedPrivkey)),
new MemoryBlock(input.rootChaincode),
)
}
if (!keyPair) {
throw new LogError("couldn't create KeyPairEd25519 from input")
}
this._ed25519KeyPair = keyPair
}
/**
* copy keys to community entity
* @param community
*/
public fillInCommunityKeys(community: Community) {
const secretBox = this.createSecretBox(community.iotaTopic)
community.rootPubkey = this._ed25519KeyPair.getPublicKey()?.data()
community.rootEncryptedPrivkey = this._ed25519KeyPair.getCryptedPrivKey(secretBox).data()
community.rootChaincode = this._ed25519KeyPair.getChainCode()?.data()
}
public get publicKey(): Buffer {
const publicKey = this._ed25519KeyPair.getPublicKey()
if (!publicKey) {
throw new LogError('invalid key pair, get empty public key')
}
return publicKey.data()
}
public get keyPair(): KeyPairEd25519 {
return this._ed25519KeyPair
}
public derive(path: number[]): KeyPair {
return new KeyPair(
path.reduce(
(keyPair: KeyPairEd25519, node: number) => keyPair.deriveChild(node),
this._ed25519KeyPair,
),
)
}
public sign(message: Buffer): Buffer {
return this._ed25519KeyPair.sign(new MemoryBlock(message)).data()
}
public static verify(message: Buffer, signaturePair: SignaturePair): boolean {
const publicKeyPair = new KeyPairEd25519(signaturePair.getPubkey())
const signature = signaturePair.getSignature()
if (!signature) {
throw new LogError('missing signature')
}
return publicKeyPair.verify(new MemoryBlock(message), signature)
}
private createSecretBox(salt: string): SecretKeyCryptography {
if (!CONFIG.GRADIDO_BLOCKCHAIN_PRIVATE_KEY_ENCRYPTION_PASSWORD) {
throw new LogError(
'missing GRADIDO_BLOCKCHAIN_PRIVATE_KEY_ENCRYPTION_PASSWORD in env or config',
)
}
const secretBox = new SecretKeyCryptography()
secretBox.createKey(salt, CONFIG.GRADIDO_BLOCKCHAIN_PRIVATE_KEY_ENCRYPTION_PASSWORD)
return secretBox
}
}

View File

@ -1,153 +0,0 @@
import { Account } from '@entity/Account'
import { Community } from '@entity/Community'
import { Transaction } from '@entity/Transaction'
import {
GradidoTransaction,
InteractionSerialize,
InteractionToJson,
TransactionBody,
} from 'gradido-blockchain-js'
import { UserIdentifier } from '@/graphql/input/UserIdentifier'
import { LogError } from '@/server/LogError'
import { CommunityRepository } from './Community.repository'
export class TransactionBuilder {
private transaction: Transaction
// https://refactoring.guru/design-patterns/builder/typescript/example
/**
* A fresh builder instance should contain a blank product object, which is
* used in further assembly.
*/
constructor() {
this.reset()
}
public reset(): void {
this.transaction = Transaction.create()
}
/**
* Concrete Builders are supposed to provide their own methods for
* retrieving results. That's because various types of builders may create
* entirely different products that don't follow the same interface.
* Therefore, such methods cannot be declared in the base Builder interface
* (at least in a statically typed programming language).
*
* Usually, after returning the end result to the client, a builder instance
* is expected to be ready to start producing another product. That's why
* it's a usual practice to call the reset method at the end of the
* `getProduct` method body. However, this behavior is not mandatory, and
* you can make your builders wait for an explicit reset call from the
* client code before disposing of the previous result.
*/
public build(): Transaction {
const result = this.transaction
this.reset()
return result
}
// return transaction without calling reset
public getTransaction(): Transaction {
return this.transaction
}
public getCommunity(): Community {
return this.transaction.community
}
public getOtherCommunity(): Community | undefined {
return this.transaction.otherCommunity
}
public setSigningAccount(signingAccount: Account): TransactionBuilder {
this.transaction.signingAccount = signingAccount
return this
}
public setRecipientAccount(recipientAccount: Account): TransactionBuilder {
this.transaction.recipientAccount = recipientAccount
return this
}
public setCommunity(community: Community): TransactionBuilder {
this.transaction.community = community
return this
}
public setOtherCommunity(otherCommunity?: Community): TransactionBuilder {
if (!this.transaction.community) {
throw new LogError('Please set community first!')
}
this.transaction.otherCommunity =
otherCommunity &&
this.transaction.community &&
this.transaction.community.id !== otherCommunity.id
? otherCommunity
: undefined
return this
}
public setSignature(signature: Buffer): TransactionBuilder {
this.transaction.signature = signature
return this
}
public async setCommunityFromUser(user: UserIdentifier): Promise<TransactionBuilder> {
// get sender community
const community = await CommunityRepository.getCommunityForUserIdentifier(user)
if (!community) {
throw new LogError("couldn't find community for transaction")
}
return this.setCommunity(community)
}
public async setOtherCommunityFromUser(user: UserIdentifier): Promise<TransactionBuilder> {
// get recipient community
const otherCommunity = await CommunityRepository.getCommunityForUserIdentifier(user)
return this.setOtherCommunity(otherCommunity)
}
public fromGradidoTransaction(transaction: GradidoTransaction): TransactionBuilder {
const body = transaction.getTransactionBody()
if (!body) {
throw new LogError('missing transaction body on Gradido Transaction')
}
// set first signature
const firstSignature = transaction.getSignatureMap().getSignaturePairs().get(0).getSignature()
if (!firstSignature) {
throw new LogError('error missing first signature')
}
this.transaction.signature = firstSignature.data()
return this.fromTransactionBody(body, transaction.getBodyBytes()?.data())
}
public fromTransactionBody(
transactionBody: TransactionBody,
bodyBytes: Buffer | null | undefined,
): TransactionBuilder {
if (!bodyBytes) {
bodyBytes = new InteractionSerialize(transactionBody).run()?.data()
}
if (!bodyBytes) {
throw new LogError(
'cannot serialize TransactionBody',
JSON.parse(new InteractionToJson(transactionBody).run()),
)
}
this.transaction.type = transactionBody.getTransactionType()
this.transaction.createdAt = new Date(transactionBody.getCreatedAt().getDate())
this.transaction.protocolVersion = transactionBody.getVersionNumber()
const transferAmount = transactionBody.getTransferAmount()
this.transaction.amount = transferAmount
? transferAmount.getAmount().getGradidoCent()
: undefined
this.transaction.bodyBytes ??= bodyBytes
return this
}
}

View File

@ -1,43 +0,0 @@
import { Transaction } from '@entity/Transaction'
import { IsNull } from 'typeorm'
import { getDataSource } from '@/typeorm/DataSource'
// https://www.artima.com/articles/the-dci-architecture-a-new-vision-of-object-oriented-programming
export const TransactionRepository = getDataSource()
.getRepository(Transaction)
.extend({
findBySignature(signature: Buffer): Promise<Transaction | null> {
return this.findOneBy({ signature: Buffer.from(signature) })
},
findByMessageId(iotaMessageId: string): Promise<Transaction | null> {
return this.findOneBy({ iotaMessageId: Buffer.from(iotaMessageId, 'hex') })
},
async getNextPendingTransaction(): Promise<Transaction | null> {
return await this.findOne({
where: { iotaMessageId: IsNull() },
order: { createdAt: 'ASC' },
relations: { signingAccount: true },
})
},
findExistingTransactionAndMissingMessageIds(messageIDsHex: string[]): Promise<Transaction[]> {
return this.createQueryBuilder('Transaction')
.where('HEX(Transaction.iota_message_id) IN (:...messageIDs)', {
messageIDs: messageIDsHex,
})
.leftJoinAndSelect('Transaction.community', 'Community')
.leftJoinAndSelect('Transaction.otherCommunity', 'OtherCommunity')
.leftJoinAndSelect('Transaction.recipientAccount', 'RecipientAccount')
.leftJoinAndSelect('Transaction.backendTransactions', 'BackendTransactions')
.leftJoinAndSelect('RecipientAccount.user', 'RecipientUser')
.leftJoinAndSelect('Transaction.signingAccount', 'SigningAccount')
.leftJoinAndSelect('SigningAccount.user', 'SigningUser')
.getMany()
},
removeConfirmedTransaction(transactions: Transaction[]): Transaction[] {
return transactions.filter(
(transaction: Transaction) =>
transaction.runningHash === undefined || transaction.runningHash.length === 0,
)
},
})

View File

@ -1,18 +0,0 @@
import { User } from '@entity/User'
import { UserAccountDraft } from '@/graphql/input/UserAccountDraft'
import { KeyPair } from './KeyPair'
import { UserLogic } from './User.logic'
export class UserFactory {
static create(userAccountDraft: UserAccountDraft, parentKeys: KeyPair): User {
const user = User.create()
user.createdAt = new Date(userAccountDraft.createdAt)
user.gradidoID = userAccountDraft.user.uuid
const userLogic = new UserLogic(user)
// store generated pubkey into entity
userLogic.calculateKeyPair(parentKeys)
return user
}
}

View File

@ -1,42 +0,0 @@
import { User } from '@entity/User'
import { LogError } from '@/server/LogError'
import { hardenDerivationIndex } from '@/utils/derivationHelper'
import { uuid4ToBuffer } from '@/utils/typeConverter'
import { KeyPair } from './KeyPair'
export class UserLogic {
// eslint-disable-next-line no-useless-constructor
constructor(private user: User) {}
/**
*
* @param parentKeys from home community for own user
* @returns
*/
calculateKeyPair = (parentKeys: KeyPair): KeyPair => {
if (!this.user.gradidoID) {
throw new LogError('missing GradidoID for user.', { id: this.user.id })
}
// example gradido id: 03857ac1-9cc2-483e-8a91-e5b10f5b8d16 =>
// wholeHex: '03857ac19cc2483e8a91e5b10f5b8d16']
const wholeHex = uuid4ToBuffer(this.user.gradidoID)
const parts = []
for (let i = 0; i < 4; i++) {
parts[i] = hardenDerivationIndex(wholeHex.subarray(i * 4, (i + 1) * 4).readUInt32BE())
}
// parts: [2206563009, 2629978174, 2324817329, 2405141782]
const keyPair = parentKeys.derive(parts)
if (this.user.derive1Pubkey && this.user.derive1Pubkey.compare(keyPair.publicKey) !== 0) {
throw new LogError(
'The freshly derived public key does not correspond to the stored public key',
)
}
if (!this.user.derive1Pubkey) {
this.user.derive1Pubkey = keyPair.publicKey
}
return keyPair
}
}

View File

@ -1,32 +0,0 @@
import { Account } from '@entity/Account'
import { User } from '@entity/User'
import { FindOptionsRelations } from 'typeorm'
import { UserIdentifier } from '@/graphql/input/UserIdentifier'
import { getDataSource } from '@/typeorm/DataSource'
export const UserRepository = getDataSource()
.getRepository(User)
.extend({
async findAccountByUserIdentifier({
uuid,
accountNr,
}: UserIdentifier): Promise<Account | undefined> {
const user = await this.findOne({
where: { gradidoID: uuid, accounts: { derivationIndex: accountNr ?? 1 } },
relations: { accounts: true },
})
if (user && user.accounts?.length === 1) {
const account = user.accounts[0]
account.user = user
return account
}
},
findByGradidoId(
{ uuid }: UserIdentifier,
relations?: FindOptionsRelations<User>,
): Promise<User | null> {
return User.findOne({ where: { gradidoID: uuid }, relations })
},
})

View File

@ -1 +0,0 @@
export const TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY = 'transmitToIota'

View File

@ -1,13 +0,0 @@
/**
* based on TransactionBody data oneOf
* https://github.com/gradido/gradido_protocol/blob/master/proto/gradido/transaction_body.proto
* for storing type in db as number
*/
export enum TransactionType {
GRADIDO_CREATION = 1,
GRADIDO_TRANSFER = 2,
GROUP_FRIENDS_UPDATE = 3,
REGISTER_ADDRESS = 4,
GRADIDO_DEFERRED_TRANSFER = 5,
COMMUNITY_ROOT = 6,
}

View File

@ -1,11 +1,8 @@
// https://www.npmjs.com/package/@apollo/protobufjs
import { IsEnum, IsObject, IsPositive, ValidateNested } from 'class-validator'
import { Decimal } from 'decimal.js-light'
import { InputType, Field, Int } from 'type-graphql'
import { InputTransactionType } from '@enum/InputTransactionType'
import { isValidDateString } from '@validator/DateString'
import { IsPositiveDecimal } from '@validator/Decimal'
import { isValidDateString, isValidNumberString } from '@validator/DateString'
import { IsEnum, IsObject, IsPositive, ValidateNested } from 'class-validator'
import { InputType, Field, Int } from 'type-graphql'
import { UserIdentifier } from './UserIdentifier'
@ -25,9 +22,9 @@ export class TransactionDraft {
@IsPositive()
backendTransactionId: number
@Field(() => Decimal)
@IsPositiveDecimal()
amount: Decimal
@Field(() => String)
@isValidNumberString()
amount: string
@Field(() => InputTransactionType)
@IsEnum(InputTransactionType)

View File

@ -1,10 +1,9 @@
// https://www.npmjs.com/package/@apollo/protobufjs
import { isValidDateString } from '@validator/DateString'
import { IsEnum, IsObject, ValidateNested } from 'class-validator'
import { InputType, Field } from 'type-graphql'
import { isValidDateString } from '@validator/DateString'
import { AccountType } from '@/graphql/enum/AccountType'
import { UserIdentifier } from './UserIdentifier'

View File

@ -1,16 +1,12 @@
/* eslint-disable camelcase */
import { AddressType_NONE } from 'gradido-blockchain-js'
import { Arg, Mutation, Query, Resolver } from 'type-graphql'
import { QueryFailedError } from 'typeorm'
import { TransactionRecipe } from '@model/TransactionRecipe'
import { TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY } from '@/data/const'
import { UserRepository } from '@/data/User.repository'
import { RegisterAddressContext } from '@/interactions/backendToDb/account/RegisterAddress.context'
import { AccountLoggingView } from '@/logging/AccountLogging.view'
import { getAddressType } from '@/client/GradidoNode'
import { KeyPairCalculation } from '@/interactions/keyPairCalculation/KeyPairCalculation.context'
import { SendToIotaContext } from '@/interactions/sendToIota/SendToIota.context'
import { logger } from '@/logging/logger'
import { TransactionLoggingView } from '@/logging/TransactionLogging.view'
import { InterruptiveSleepManager } from '@/manager/InterruptiveSleepManager'
import { getDataSource } from '@/typeorm/DataSource'
import { uuid4ToHash } from '@/utils/typeConverter'
import { TransactionErrorType } from '../enum/TransactionErrorType'
import { UserAccountDraft } from '../input/UserAccountDraft'
@ -22,8 +18,20 @@ import { TransactionResult } from '../model/TransactionResult'
export class AccountResolver {
@Query(() => Boolean)
async isAccountExist(@Arg('data') userIdentifier: UserIdentifier): Promise<boolean> {
const accountKeyPair = await KeyPairCalculation(userIdentifier)
const publicKey = accountKeyPair.getPublicKey()
if (!publicKey) {
throw new TransactionResult(
new TransactionError(TransactionErrorType.NOT_FOUND, 'cannot get user public key'),
)
}
// ask gradido node server for account type, if type !== NONE account exist
const addressType = await getAddressType(
publicKey.data(),
uuid4ToHash(userIdentifier.communityUuid).convertToHex(),
)
logger.info('isAccountExist', userIdentifier)
return !!(await UserRepository.findAccountByUserIdentifier(userIdentifier))
return addressType !== AddressType_NONE
}
@Mutation(() => TransactionResult)
@ -31,30 +39,10 @@ export class AccountResolver {
@Arg('data')
userAccountDraft: UserAccountDraft,
): Promise<TransactionResult> {
const registerAddressContext = new RegisterAddressContext(userAccountDraft)
try {
const { transaction, account } = await registerAddressContext.run()
logger.info('register address', {
account: new AccountLoggingView(account),
transaction: new TransactionLoggingView(transaction),
})
await getDataSource().transaction(async (transactionalEntityManager) => {
await transactionalEntityManager.save(account)
await transactionalEntityManager.save(transaction)
logger.debug('store register address transaction', new TransactionLoggingView(transaction))
})
InterruptiveSleepManager.getInstance().interrupt(TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY)
return new TransactionResult(new TransactionRecipe(transaction))
return await SendToIotaContext(userAccountDraft)
} catch (err) {
if (err instanceof QueryFailedError) {
logger.error('error saving user or new account or transaction into db: %s', err)
return new TransactionResult(
new TransactionError(
TransactionErrorType.DB_ERROR,
'error saving user or new account or transaction into db',
),
)
} else if (err instanceof TransactionError) {
if (err instanceof TransactionError) {
return new TransactionResult(err)
} else {
logger.error('error in register address: ', err)

View File

@ -1,72 +0,0 @@
import { CommunityArg } from '@arg/CommunityArg'
import { TransactionErrorType } from '@enum/TransactionErrorType'
import { CommunityDraft } from '@input/CommunityDraft'
import { Community } from '@model/Community'
import { TransactionError } from '@model/TransactionError'
import { TransactionResult } from '@model/TransactionResult'
import { Resolver, Query, Arg, Mutation, Args } from 'type-graphql'
import { CommunityRepository } from '@/data/Community.repository'
import { AddCommunityContext } from '@/interactions/backendToDb/community/AddCommunity.context'
import { logger } from '@/logging/logger'
import { LogError } from '@/server/LogError'
import { uuid4ToHash } from '@/utils/typeConverter'
@Resolver()
export class CommunityResolver {
@Query(() => Community)
async community(@Args() communityArg: CommunityArg): Promise<Community> {
logger.info('community', communityArg)
const result = await CommunityRepository.findByCommunityArg(communityArg)
if (result.length === 0) {
throw new LogError('cannot find community')
} else if (result.length === 1) {
return new Community(result[0])
} else {
throw new LogError('find multiple communities')
}
}
@Query(() => Boolean)
async isCommunityExist(@Args() communityArg: CommunityArg): Promise<boolean> {
logger.info('isCommunity', communityArg)
return (await CommunityRepository.findByCommunityArg(communityArg)).length === 1
}
@Query(() => [Community])
async communities(@Args() communityArg: CommunityArg): Promise<Community[]> {
logger.info('communities', communityArg)
const result = await CommunityRepository.findByCommunityArg(communityArg)
return result.map((communityEntity) => new Community(communityEntity))
}
@Mutation(() => TransactionResult)
async addCommunity(
@Arg('data')
communityDraft: CommunityDraft,
): Promise<TransactionResult> {
logger.info('addCommunity', communityDraft)
const topic = uuid4ToHash(communityDraft.uuid)
// check if community was already written to db
if (await CommunityRepository.isExist(topic)) {
return new TransactionResult(
new TransactionError(TransactionErrorType.ALREADY_EXIST, 'community already exist!'),
)
}
// prepare context for interaction
// shouldn't throw at all
// TODO: write tests to make sure that it doesn't throw
const addCommunityContext = new AddCommunityContext(communityDraft, topic)
try {
// actually run interaction, create community, accounts for foreign community and transactionRecipe
await addCommunityContext.run()
return new TransactionResult()
} catch (error) {
if (error instanceof TransactionError) {
return new TransactionResult(error)
} else {
throw error
}
}
}
}

View File

@ -1,17 +1,9 @@
import { TransactionDraft } from '@input/TransactionDraft'
import { Resolver, Arg, Mutation } from 'type-graphql'
import { TransactionDraft } from '@input/TransactionDraft'
import { SendToIotaContext } from '@/interactions/sendToIota/SendToIota.context'
import { TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY } from '@/data/const'
import { TransactionRepository } from '@/data/Transaction.repository'
import { CreateTransactionRecipeContext } from '@/interactions/backendToDb/transaction/CreateTransactionRecipe.context'
import { logger } from '@/logging/logger'
import { TransactionLoggingView } from '@/logging/TransactionLogging.view'
import { InterruptiveSleepManager } from '@/manager/InterruptiveSleepManager'
import { TransactionErrorType } from '../enum/TransactionErrorType'
import { TransactionError } from '../model/TransactionError'
import { TransactionRecipe } from '../model/TransactionRecipe'
import { TransactionResult } from '../model/TransactionResult'
@Resolver()
@ -21,36 +13,8 @@ export class TransactionResolver {
@Arg('data')
transactionDraft: TransactionDraft,
): Promise<TransactionResult> {
const createTransactionRecipeContext = new CreateTransactionRecipeContext(transactionDraft)
try {
const result = await createTransactionRecipeContext.run()
if (!result) {
return new TransactionResult(
new TransactionError(
TransactionErrorType.MISSING_PARAMETER,
'cannot work with this parameters',
),
)
}
const transactionRecipe = createTransactionRecipeContext.getTransactionRecipe()
// check if a transaction with this signature already exist
const existingRecipe = await TransactionRepository.findBySignature(
transactionRecipe.signature,
)
if (existingRecipe) {
return new TransactionResult(
new TransactionError(
TransactionErrorType.ALREADY_EXIST,
'Transaction with same signature already exist',
),
)
} else {
logger.debug('store transaction recipe', new TransactionLoggingView(transactionRecipe))
// we store the transaction
await transactionRecipe.save()
}
InterruptiveSleepManager.getInstance().interrupt(TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY)
return new TransactionResult(new TransactionRecipe(transactionRecipe))
return await SendToIotaContext(transactionDraft)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
if (error instanceof TransactionError) {

View File

@ -1,30 +0,0 @@
/* eslint-disable @typescript-eslint/no-unsafe-argument */
import { Decimal } from 'decimal.js-light'
import { GraphQLScalarType, Kind, ValueNode } from 'graphql'
export const DecimalScalar = new GraphQLScalarType({
name: 'Decimal',
description: 'The `Decimal` scalar type to represent currency values',
serialize(value: unknown): string {
if (!(value instanceof Decimal)) {
throw new TypeError(`Value is not a Decimal: ${value}`)
}
return value.toString()
},
parseValue(value: unknown): Decimal {
if (typeof value !== 'string') {
throw new TypeError('Decimal values must be strings')
}
return new Decimal(value)
},
parseLiteral(ast: ValueNode): Decimal {
if (ast.kind !== Kind.STRING) {
throw new TypeError(`${String(ast)} is not a valid decimal value.`)
}
return new Decimal(ast.value)
},
})

View File

@ -1,16 +1,12 @@
import { Decimal } from 'decimal.js-light'
import { GraphQLSchema } from 'graphql'
import { buildSchema } from 'type-graphql'
import { AccountResolver } from './resolver/AccountsResolver'
import { CommunityResolver } from './resolver/CommunityResolver'
import { TransactionResolver } from './resolver/TransactionsResolver'
import { DecimalScalar } from './scalar/Decimal'
export const schema = async (): Promise<GraphQLSchema> => {
return buildSchema({
resolvers: [TransactionResolver, CommunityResolver, AccountResolver],
scalarsMap: [{ type: Decimal, scalar: DecimalScalar }],
resolvers: [TransactionResolver, AccountResolver],
validate: {
validationError: { target: false },
skipMissingProperties: true,

View File

@ -19,3 +19,23 @@ export function isValidDateString(validationOptions?: ValidationOptions) {
})
}
}
export function isValidNumberString(validationOptions?: ValidationOptions) {
// eslint-disable-next-line @typescript-eslint/ban-types
return function (object: Object, propertyName: string) {
registerDecorator({
name: 'isValidNumberString',
target: object.constructor,
propertyName,
options: validationOptions,
validator: {
validate(value: string): boolean {
return !isNaN(parseFloat(value))
},
defaultMessage(): string {
return `${propertyName} must be a valid number string`
},
},
})
}
}

View File

@ -1,22 +0,0 @@
import { registerDecorator, ValidationOptions, ValidationArguments } from 'class-validator'
import { Decimal } from 'decimal.js-light'
export function IsPositiveDecimal(validationOptions?: ValidationOptions) {
// eslint-disable-next-line @typescript-eslint/ban-types
return function (object: Object, propertyName: string) {
registerDecorator({
name: 'isPositiveDecimal',
target: object.constructor,
propertyName,
options: validationOptions,
validator: {
validate(value: Decimal): boolean {
return value.greaterThan(0)
},
defaultMessage(args: ValidationArguments): string {
return `The ${propertyName} must be a positive value ${args.property}`
},
},
})
}
}

View File

@ -7,12 +7,13 @@ import { CONFIG } from '@/config'
import { BackendClient } from './client/BackendClient'
import { CommunityDraft } from './graphql/input/CommunityDraft'
import { AddCommunityContext } from './interactions/backendToDb/community/AddCommunity.context'
import { logger } from './logging/logger'
import { KeyPairCacheManager } from './manager/KeyPairCacheManager'
import createServer from './server/createServer'
import { LogError } from './server/LogError'
import { stopTransmitToIota, transmitToIota } from './tasks/transmitToIota'
import { getTransaction } from './client/GradidoNode'
import { uuid4ToHash } from './utils/typeConverter'
import { SendToIotaContext } from './interactions/sendToIota/SendToIota.context'
async function waitForServer(
backend: BackendClient,
@ -70,11 +71,12 @@ async function main() {
const communityDraft = await backend.getHomeCommunityDraft()
KeyPairCacheManager.getInstance().setHomeCommunityUUID(communityDraft.uuid)
const addCommunityContext = new AddCommunityContext(communityDraft)
await addCommunityContext.run()
// loop run all the time, check for new transaction for sending to iota
void transmitToIota()
// ask gradido node if community blockchain was created
const firstTransaction = await getTransaction(1, uuid4ToHash(communityDraft.uuid).convertToHex())
if (!firstTransaction) {
// if not exist, create community root transaction
await SendToIotaContext(communityDraft)
}
app.listen(CONFIG.DLT_CONNECTOR_PORT, () => {
// eslint-disable-next-line no-console
console.log(`Server is running at http://localhost:${CONFIG.DLT_CONNECTOR_PORT}`)
@ -82,7 +84,6 @@ async function main() {
process.on('exit', () => {
// Add shutdown logic here.
stopTransmitToIota()
})
}

View File

@ -1,65 +0,0 @@
import { Account } from '@entity/Account'
import { Transaction } from '@entity/Transaction'
import { User } from '@entity/User'
import { AccountFactory } from '@/data/Account.factory'
import { CommunityRepository } from '@/data/Community.repository'
import { KeyPair } from '@/data/KeyPair'
import { UserFactory } from '@/data/User.factory'
import { UserLogic } from '@/data/User.logic'
import { UserRepository } from '@/data/User.repository'
import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType'
import { UserAccountDraft } from '@/graphql/input/UserAccountDraft'
import { TransactionError } from '@/graphql/model/TransactionError'
import { logger } from '@/logging/logger'
import { CreateTransactionRecipeContext } from '../transaction/CreateTransactionRecipe.context'
export interface TransactionWithAccount {
transaction: Transaction
account: Account
}
export class RegisterAddressContext {
// eslint-disable-next-line no-useless-constructor
public constructor(private userAccountDraft: UserAccountDraft) {}
public async run(): Promise<TransactionWithAccount> {
const community = await CommunityRepository.loadHomeCommunity()
const communityKeyPair = new KeyPair(community)
const user = await this.loadOrCreateUser(communityKeyPair)
if (this.isAccountAlreadyExistOnUser(user)) {
throw new TransactionError(
TransactionErrorType.ALREADY_EXIST,
'account for this user already exist!',
)
}
logger.info('add user and account', this.userAccountDraft)
const account = this.createAccount(new UserLogic(user).calculateKeyPair(communityKeyPair))
account.user = user
const createTransactionContext = new CreateTransactionRecipeContext(this.userAccountDraft, {
community,
account,
})
await createTransactionContext.run()
return { transaction: createTransactionContext.getTransactionRecipe(), account }
}
public isAccountAlreadyExistOnUser(user: User): boolean {
return !!user.accounts?.find(
(value) => value.derivationIndex === this.userAccountDraft.user.accountNr,
)
}
public async loadOrCreateUser(communityKeyPair: KeyPair): Promise<User> {
let user = await UserRepository.findByGradidoId(this.userAccountDraft.user, { accounts: true })
if (!user) {
user = UserFactory.create(this.userAccountDraft, communityKeyPair)
}
return user
}
public createAccount(userKeyPair: KeyPair): Account {
return AccountFactory.createAccountFromUserAccountDraft(this.userAccountDraft, userKeyPair)
}
}

View File

@ -1,65 +0,0 @@
import 'reflect-metadata'
import { Community } from '@entity/Community'
import { TestDB } from '@test/TestDB'
import { CONFIG } from '@/config'
import { CommunityDraft } from '@/graphql/input/CommunityDraft'
import { AddCommunityContext } from './AddCommunity.context'
CONFIG.IOTA_HOME_COMMUNITY_SEED = '034b0229a2ba4e98e1cc5e8767dca886279b484303ffa73546bd5f5bf0b71285'
jest.mock('@typeorm/DataSource', () => ({
getDataSource: jest.fn(() => TestDB.instance.dbConnect),
}))
describe('interactions/backendToDb/community/AddCommunity Context Test', () => {
beforeAll(async () => {
await TestDB.instance.setupTestDB()
})
afterAll(async () => {
await TestDB.instance.teardownTestDB()
})
const homeCommunityDraft = new CommunityDraft()
homeCommunityDraft.uuid = 'a2fd0fee-f3ba-4bef-a62a-10a34b0e2754'
homeCommunityDraft.foreign = false
homeCommunityDraft.createdAt = '2024-01-25T13:09:55.339Z'
// calculated from a2fd0fee-f3ba-4bef-a62a-10a34b0e2754 with iotaTopicFromCommunityUUID
const iotaTopic = '7be2ad83f279a3aaf6d62371cb6be301e2e3c7a3efda9c89984e8f6a7865d9ce'
const foreignCommunityDraft = new CommunityDraft()
foreignCommunityDraft.uuid = '70df8de5-0fb7-4153-a124-4ff86965be9a'
foreignCommunityDraft.foreign = true
foreignCommunityDraft.createdAt = '2024-01-25T13:34:28.020Z'
it('with home community, without iota topic', async () => {
const context = new AddCommunityContext(homeCommunityDraft)
await context.run()
const homeCommunity = await Community.findOneOrFail({ where: { iotaTopic } })
expect(homeCommunity).toMatchObject({
id: 1,
iotaTopic,
foreign: 0,
rootPubkey: Buffer.from(
'07cbf56d4b6b7b188c5f6250c0f4a01d0e44e1d422db1935eb375319ad9f9af0',
'hex',
),
createdAt: new Date('2024-01-25T13:09:55.339Z'),
})
})
it('with foreign community', async () => {
const context = new AddCommunityContext(foreignCommunityDraft, 'randomTopic')
await context.run()
const foreignCommunity = await Community.findOneOrFail({ where: { foreign: true } })
expect(foreignCommunity).toMatchObject({
id: 2,
iotaTopic: 'randomTopic',
foreign: 1,
createdAt: new Date('2024-01-25T13:34:28.020Z'),
})
})
})

View File

@ -1,31 +0,0 @@
import { CommunityDraft } from '@/graphql/input/CommunityDraft'
import { uuid4ToHash } from '@/utils/typeConverter'
import { CommunityRole } from './Community.role'
import { ForeignCommunityRole } from './ForeignCommunity.role'
import { HomeCommunityRole } from './HomeCommunity.role'
/**
* @DCI-Context
* Context for adding community to DB
* using roles to distinct between foreign and home communities
*/
export class AddCommunityContext {
private communityRole: CommunityRole
private iotaTopic: string
public constructor(private communityDraft: CommunityDraft, iotaTopic?: string) {
if (!iotaTopic) {
this.iotaTopic = uuid4ToHash(this.communityDraft.uuid)
} else {
this.iotaTopic = iotaTopic
}
this.communityRole = communityDraft.foreign
? new ForeignCommunityRole()
: new HomeCommunityRole()
}
public async run(): Promise<void> {
await this.communityRole.create(this.communityDraft, this.iotaTopic)
await this.communityRole.store()
}
}

View File

@ -1,31 +0,0 @@
import { Community } from '@entity/Community'
import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType'
import { CommunityDraft } from '@/graphql/input/CommunityDraft'
import { TransactionError } from '@/graphql/model/TransactionError'
import { CommunityLoggingView } from '@/logging/CommunityLogging.view'
import { logger } from '@/logging/logger'
export abstract class CommunityRole {
protected self: Community
public constructor() {
this.self = Community.create()
}
public async create(communityDraft: CommunityDraft, topic: string): Promise<void> {
this.self.iotaTopic = topic
this.self.createdAt = new Date(communityDraft.createdAt)
this.self.foreign = communityDraft.foreign
}
public async store(): Promise<Community> {
try {
const community = await this.self.save()
logger.debug('store community', new CommunityLoggingView(community))
return community
} catch (error) {
logger.error('error saving new community into db: %s', error)
throw new TransactionError(TransactionErrorType.DB_ERROR, 'error saving community into db')
}
}
}

View File

@ -1,4 +0,0 @@
import { CommunityRole } from './Community.role'
// same as base class
export class ForeignCommunityRole extends CommunityRole {}

View File

@ -1,73 +0,0 @@
import { Community } from '@entity/Community'
import { Transaction } from '@entity/Transaction'
import { CONFIG } from '@/config'
import { AccountFactory } from '@/data/Account.factory'
import { TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY } from '@/data/const'
import { KeyPair } from '@/data/KeyPair'
import { Mnemonic } from '@/data/Mnemonic'
import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType'
import { CommunityDraft } from '@/graphql/input/CommunityDraft'
import { TransactionError } from '@/graphql/model/TransactionError'
import { CommunityLoggingView } from '@/logging/CommunityLogging.view'
import { logger } from '@/logging/logger'
import { InterruptiveSleepManager } from '@/manager/InterruptiveSleepManager'
import { LogError } from '@/server/LogError'
import { getDataSource } from '@/typeorm/DataSource'
import { CreateTransactionRecipeContext } from '../transaction/CreateTransactionRecipe.context'
import { CommunityRole } from './Community.role'
import { TransactionLoggingView } from '@/logging/TransactionLogging.view'
export class HomeCommunityRole extends CommunityRole {
private transactionRecipe: Transaction
public async create(communityDraft: CommunityDraft, topic: string): Promise<void> {
super.create(communityDraft, topic)
// generate key pair for signing transactions and deriving all keys for community
let mnemonic: Mnemonic
try {
mnemonic = new Mnemonic(CONFIG.IOTA_HOME_COMMUNITY_SEED ?? undefined)
} catch (e) {
throw new LogError(
'error creating mnemonic for home community, please fill IOTA_HOME_COMMUNITY_SEED in .env',
{
IOTA_HOME_COMMUNITY_SEED: CONFIG.IOTA_HOME_COMMUNITY_SEED,
error: e,
},
)
}
const keyPair = new KeyPair(mnemonic)
keyPair.fillInCommunityKeys(this.self)
// create auf account and gmw account
this.self.aufAccount = AccountFactory.createAufAccount(keyPair, this.self.createdAt)
this.self.gmwAccount = AccountFactory.createGmwAccount(keyPair, this.self.createdAt)
const transactionRecipeContext = new CreateTransactionRecipeContext(communityDraft, {
community: this.self,
})
await transactionRecipeContext.run()
this.transactionRecipe = transactionRecipeContext.getTransactionRecipe()
}
public async store(): Promise<Community> {
try {
const community = await getDataSource().transaction(async (transactionalEntityManager) => {
const community = await transactionalEntityManager.save(this.self)
await transactionalEntityManager.save(this.transactionRecipe)
logger.debug('store home community', new CommunityLoggingView(community))
return community
})
InterruptiveSleepManager.getInstance().interrupt(TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY)
return community
} catch (error) {
logger.error('error saving home community into db: %s', error)
throw new TransactionError(
TransactionErrorType.DB_ERROR,
'error saving home community into db',
)
}
}
}

View File

@ -1,36 +0,0 @@
/* eslint-disable camelcase */
import { Account } from '@entity/Account'
import { GradidoTransactionBuilder } from 'gradido-blockchain-js'
import { UserRepository } from '@/data/User.repository'
import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType'
import { TransactionDraft } from '@/graphql/input/TransactionDraft'
import { UserIdentifier } from '@/graphql/input/UserIdentifier'
import { TransactionError } from '@/graphql/model/TransactionError'
export abstract class AbstractTransactionRole {
// eslint-disable-next-line no-useless-constructor
public constructor(protected self: TransactionDraft) {}
abstract getSigningUser(): UserIdentifier
abstract getRecipientUser(): UserIdentifier
abstract getGradidoTransactionBuilder(): Promise<GradidoTransactionBuilder>
public isCrossGroupTransaction(): boolean {
return (
this.self.user.communityUuid !== this.self.linkedUser.communityUuid &&
this.self.linkedUser.communityUuid !== ''
)
}
public async loadUser(user: UserIdentifier): Promise<Account> {
const account = await UserRepository.findAccountByUserIdentifier(user)
if (!account) {
throw new TransactionError(
TransactionErrorType.NOT_FOUND,
"couldn't found user account in db",
)
}
return account
}
}

View File

@ -1,15 +0,0 @@
import { Transaction } from '@entity/Transaction'
import { TransactionBuilder } from '@/data/Transaction.builder'
export class AbstractTransactionRecipeRole {
protected transactionBuilder: TransactionBuilder
public constructor() {
this.transactionBuilder = new TransactionBuilder()
}
public getTransaction(): Transaction {
return this.transactionBuilder.getTransaction()
}
}

View File

@ -1,40 +0,0 @@
/* eslint-disable camelcase */
import { AccountLogic } from '@/data/Account.logic'
import { KeyPair } from '@/data/KeyPair'
import { TransactionDraft } from '@/graphql/input/TransactionDraft'
import { AbstractTransactionRole } from './AbstractTransaction.role'
import { AbstractTransactionRecipeRole } from './AbstractTransactionRecipeRole'
export class BalanceChangingTransactionRecipeRole extends AbstractTransactionRecipeRole {
public async create(
transactionDraft: TransactionDraft,
transactionTypeRole: AbstractTransactionRole,
): Promise<BalanceChangingTransactionRecipeRole> {
// loading signing and recipient account
const signingAccount = await transactionTypeRole.loadUser(transactionTypeRole.getSigningUser())
const recipientAccount = await transactionTypeRole.loadUser(
transactionTypeRole.getRecipientUser(),
)
const accountLogic = new AccountLogic(signingAccount)
await this.transactionBuilder.setCommunityFromUser(transactionDraft.user)
const communityKeyPair = new KeyPair(this.transactionBuilder.getCommunity())
const gradidoTransactionBuilder = await transactionTypeRole.getGradidoTransactionBuilder()
const transaction = gradidoTransactionBuilder
.setCreatedAt(new Date(transactionDraft.createdAt))
.sign(accountLogic.calculateKeyPair(communityKeyPair).keyPair)
.build()
// build transaction entity
this.transactionBuilder
.fromGradidoTransaction(transaction)
.setRecipientAccount(recipientAccount)
.setSigningAccount(signingAccount)
if (transactionTypeRole.isCrossGroupTransaction()) {
await this.transactionBuilder.setOtherCommunityFromUser(transactionDraft.linkedUser)
}
return this
}
}

View File

@ -1,38 +0,0 @@
import { Community } from '@entity/Community'
// eslint-disable-next-line camelcase
import { MemoryBlock, GradidoTransactionBuilder } from 'gradido-blockchain-js'
import { KeyPair } from '@/data/KeyPair'
// import { TransactionBodyBuilder } from '@/data/proto/TransactionBody.builder'
import { CommunityDraft } from '@/graphql/input/CommunityDraft'
import { AbstractTransactionRecipeRole } from './AbstractTransactionRecipeRole'
export class CommunityRootTransactionRole extends AbstractTransactionRecipeRole {
public create(
communityDraft: CommunityDraft,
community: Community,
): AbstractTransactionRecipeRole {
if (
!community.rootPubkey ||
!community.gmwAccount?.derive2Pubkey ||
!community.aufAccount?.derive2Pubkey
) {
throw new Error('missing one of the public keys for community')
}
// create proto transaction body
const transaction = new GradidoTransactionBuilder()
.setCommunityRoot(
new MemoryBlock(community.rootPubkey),
new MemoryBlock(community.gmwAccount?.derive2Pubkey),
new MemoryBlock(community.aufAccount?.derive2Pubkey),
)
.setCreatedAt(new Date(communityDraft.createdAt))
.sign(new KeyPair(community).keyPair)
.build()
// build transaction entity
this.transactionBuilder.fromGradidoTransaction(transaction).setCommunity(community)
return this
}
}

View File

@ -1,420 +0,0 @@
/* eslint-disable camelcase */
import 'reflect-metadata'
import { Account } from '@entity/Account'
import { Community } from '@entity/Community'
import {
AddressType_COMMUNITY_HUMAN,
CrossGroupType_INBOUND,
CrossGroupType_LOCAL,
CrossGroupType_OUTBOUND,
InteractionDeserialize,
MemoryBlock,
TransactionType_CREATION,
} from 'gradido-blockchain-js'
import { v4 } from 'uuid'
import { TestDB } from '@test/TestDB'
import { CONFIG } from '@/config'
import { KeyPair } from '@/data/KeyPair'
import { TransactionType } from '@/data/proto/3_3/enum/TransactionType'
import { AccountType } from '@/graphql/enum/AccountType'
import { InputTransactionType } from '@/graphql/enum/InputTransactionType'
import { TransactionDraft } from '@/graphql/input/TransactionDraft'
import { UserAccountDraft } from '@/graphql/input/UserAccountDraft'
import { uuid4ToHash } from '@/utils/typeConverter'
import { CreateTransactionRecipeContext } from './CreateTransactionRecipe.context'
// eslint-disable-next-line import/order
import { communitySeed } from '@test/seeding/Community.seed'
// eslint-disable-next-line import/order
import { createUserSet, UserSet } from '@test/seeding/UserSet.seed'
jest.mock('@typeorm/DataSource', () => ({
getDataSource: jest.fn(() => TestDB.instance.dbConnect),
}))
CONFIG.IOTA_HOME_COMMUNITY_SEED = '034b0229a2ba4e98e1cc5e8767dca886279b484303ffa73546bd5f5bf0b71285'
const homeCommunityUuid = v4()
const foreignCommunityUuid = v4()
const keyPair = new KeyPair(MemoryBlock.fromHex(CONFIG.IOTA_HOME_COMMUNITY_SEED))
const foreignKeyPair = new KeyPair(
MemoryBlock.fromHex('5d4e163c078cc6b51f5c88f8422bc8f21d1d59a284515ab1ea79e1c176ebec50'),
)
let moderator: UserSet
let firstUser: UserSet
let secondUser: UserSet
let foreignUser: UserSet
let homeCommunity: Community
const topic = uuid4ToHash(homeCommunityUuid)
const foreignTopic = uuid4ToHash(foreignCommunityUuid)
describe('interactions/backendToDb/transaction/Create Transaction Recipe Context Test', () => {
beforeAll(async () => {
await TestDB.instance.setupTestDB()
homeCommunity = await communitySeed(homeCommunityUuid, false)
await communitySeed(foreignCommunityUuid, true, foreignKeyPair)
moderator = createUserSet(homeCommunityUuid, keyPair)
firstUser = createUserSet(homeCommunityUuid, keyPair)
secondUser = createUserSet(homeCommunityUuid, keyPair)
foreignUser = createUserSet(foreignCommunityUuid, foreignKeyPair)
await Account.save([
moderator.account,
firstUser.account,
secondUser.account,
foreignUser.account,
])
})
afterAll(async () => {
await TestDB.instance.teardownTestDB()
})
it('register address transaction', async () => {
const userAccountDraft = new UserAccountDraft()
userAccountDraft.accountType = AccountType.COMMUNITY_HUMAN
userAccountDraft.createdAt = new Date().toISOString()
userAccountDraft.user = firstUser.identifier
const context = new CreateTransactionRecipeContext(userAccountDraft, {
account: firstUser.account,
community: homeCommunity,
})
await context.run()
const transaction = context.getTransactionRecipe()
expect(transaction).toMatchObject({
type: TransactionType.REGISTER_ADDRESS,
protocolVersion: '3.3',
community: {
rootPubkey: keyPair.publicKey,
foreign: 0,
iotaTopic: topic,
},
signingAccount: {
derive2Pubkey: firstUser.account.derive2Pubkey,
},
})
const deserializer = new InteractionDeserialize(new MemoryBlock(transaction.bodyBytes))
deserializer.run()
const body = deserializer.getTransactionBody()
expect(body).not.toBeNull()
expect(body?.isRegisterAddress()).toBeTruthy()
expect(body).toMatchObject({
type: CrossGroupType_LOCAL,
registerAddress: {
derivationIndex: 1,
addressType: AddressType_COMMUNITY_HUMAN,
},
})
})
it('creation transaction', async () => {
const creationTransactionDraft = new TransactionDraft()
creationTransactionDraft.amount = new Decimal('2000')
creationTransactionDraft.backendTransactionId = 1
creationTransactionDraft.createdAt = new Date().toISOString()
creationTransactionDraft.linkedUser = moderator.identifier
creationTransactionDraft.user = firstUser.identifier
creationTransactionDraft.type = InputTransactionType.CREATION
creationTransactionDraft.targetDate = new Date().toISOString()
const context = new CreateTransactionRecipeContext(creationTransactionDraft)
await context.run()
const transaction = context.getTransactionRecipe()
expect(transaction).toMatchObject({
type: TransactionType_CREATION,
protocolVersion: '3.3',
community: {
rootPubkey: keyPair.publicKey,
foreign: 0,
iotaTopic: topic,
},
signingAccount: {
derive2Pubkey: moderator.account.derive2Pubkey,
},
recipientAccount: {
derive2Pubkey: firstUser.account.derive2Pubkey,
},
amount: new Decimal(2000),
backendTransactions: [
{
typeId: InputTransactionType.CREATION,
},
],
})
const deserializer = new InteractionDeserialize(new MemoryBlock(transaction.bodyBytes))
deserializer.run()
const body = deserializer.getTransactionBody()
expect(body).not.toBeNull()
// console.log(new TransactionBodyLoggingView(body))
expect(body?.isCreation()).toBeTruthy()
expect(
body
?.getCreation()
?.getRecipient()
.getPubkey()
?.equal(new MemoryBlock(firstUser.account.derive2Pubkey)),
).toBeTruthy()
expect(body).toMatchObject({
type: CrossGroupType_LOCAL,
creation: {
recipient: {
amount: '2000',
},
},
})
})
it('local send transaction', async () => {
const sendTransactionDraft = new TransactionDraft()
sendTransactionDraft.amount = new Decimal('100')
sendTransactionDraft.backendTransactionId = 2
sendTransactionDraft.createdAt = new Date().toISOString()
sendTransactionDraft.linkedUser = secondUser.identifier
sendTransactionDraft.user = firstUser.identifier
sendTransactionDraft.type = InputTransactionType.SEND
const context = new CreateTransactionRecipeContext(sendTransactionDraft)
await context.run()
const transaction = context.getTransactionRecipe()
// console.log(new TransactionLoggingView(transaction))
expect(transaction).toMatchObject({
type: TransactionType.GRADIDO_TRANSFER,
protocolVersion: '3.3',
community: {
rootPubkey: keyPair.publicKey,
foreign: 0,
iotaTopic: topic,
},
signingAccount: {
derive2Pubkey: firstUser.account.derive2Pubkey,
},
recipientAccount: {
derive2Pubkey: secondUser.account.derive2Pubkey,
},
amount: new Decimal(100),
backendTransactions: [
{
typeId: InputTransactionType.SEND,
},
],
})
const deserializer = new InteractionDeserialize(new MemoryBlock(transaction.bodyBytes))
deserializer.run()
const body = deserializer.getTransactionBody()
expect(body).not.toBeNull()
// console.log(new TransactionBodyLoggingView(body))
expect(body?.isTransfer()).toBeTruthy()
const transfer = body?.getTransfer()
expect(transfer).not.toBeNull()
expect(
transfer?.getRecipient()?.equal(new MemoryBlock(secondUser.account.derive2Pubkey)),
).toBeTruthy()
expect(
transfer?.getSender().getPubkey()?.equal(new MemoryBlock(firstUser.account.derive2Pubkey)),
).toBeTruthy()
expect(body).toMatchObject({
type: CrossGroupType_LOCAL,
transfer: {
sender: {
amount: '100',
},
},
})
})
it('local recv transaction', async () => {
const recvTransactionDraft = new TransactionDraft()
recvTransactionDraft.amount = new Decimal('100')
recvTransactionDraft.backendTransactionId = 3
recvTransactionDraft.createdAt = new Date().toISOString()
recvTransactionDraft.linkedUser = firstUser.identifier
recvTransactionDraft.user = secondUser.identifier
recvTransactionDraft.type = InputTransactionType.RECEIVE
const context = new CreateTransactionRecipeContext(recvTransactionDraft)
await context.run()
const transaction = context.getTransactionRecipe()
// console.log(new TransactionLoggingView(transaction))
expect(transaction).toMatchObject({
type: TransactionType.GRADIDO_TRANSFER,
protocolVersion: '3.3',
community: {
rootPubkey: keyPair.publicKey,
foreign: 0,
iotaTopic: topic,
},
signingAccount: {
derive2Pubkey: firstUser.account.derive2Pubkey,
},
recipientAccount: {
derive2Pubkey: secondUser.account.derive2Pubkey,
},
amount: new Decimal(100),
backendTransactions: [
{
typeId: InputTransactionType.RECEIVE,
},
],
})
const deserializer = new InteractionDeserialize(new MemoryBlock(transaction.bodyBytes))
deserializer.run()
const body = deserializer.getTransactionBody()
expect(body).not.toBeNull()
expect(body?.isTransfer()).toBeTruthy()
const transfer = body?.getTransfer()
expect(transfer).not.toBeNull()
expect(
transfer?.getRecipient()?.equal(new MemoryBlock(secondUser.account.derive2Pubkey)),
).toBeTruthy()
expect(
transfer?.getSender().getPubkey()?.equal(new MemoryBlock(firstUser.account.derive2Pubkey)),
).toBeTruthy()
expect(body).toMatchObject({
type: CrossGroupType_LOCAL,
transfer: {
sender: {
amount: '100',
},
},
})
})
it('cross group send transaction', async () => {
const crossGroupSendTransactionDraft = new TransactionDraft()
crossGroupSendTransactionDraft.amount = new Decimal('100')
crossGroupSendTransactionDraft.backendTransactionId = 4
crossGroupSendTransactionDraft.createdAt = new Date().toISOString()
crossGroupSendTransactionDraft.linkedUser = foreignUser.identifier
crossGroupSendTransactionDraft.user = firstUser.identifier
crossGroupSendTransactionDraft.type = InputTransactionType.SEND
const context = new CreateTransactionRecipeContext(crossGroupSendTransactionDraft)
await context.run()
const transaction = context.getTransactionRecipe()
// console.log(new TransactionLoggingView(transaction))
expect(transaction).toMatchObject({
type: TransactionType.GRADIDO_TRANSFER,
protocolVersion: '3.3',
community: {
rootPubkey: keyPair.publicKey,
foreign: 0,
iotaTopic: topic,
},
otherCommunity: {
rootPubkey: foreignKeyPair.publicKey,
foreign: 1,
iotaTopic: foreignTopic,
},
signingAccount: {
derive2Pubkey: firstUser.account.derive2Pubkey,
},
recipientAccount: {
derive2Pubkey: foreignUser.account.derive2Pubkey,
},
amount: new Decimal(100),
backendTransactions: [
{
typeId: InputTransactionType.SEND,
},
],
})
const deserializer = new InteractionDeserialize(new MemoryBlock(transaction.bodyBytes))
deserializer.run()
const body = deserializer.getTransactionBody()
expect(body).not.toBeNull()
// console.log(new TransactionBodyLoggingView(body))
expect(body?.isTransfer()).toBeTruthy()
const transfer = body?.getTransfer()
expect(transfer).not.toBeNull()
expect(
transfer?.getRecipient()?.equal(new MemoryBlock(foreignUser.account.derive2Pubkey)),
).toBeTruthy()
expect(
transfer?.getSender().getPubkey()?.equal(new MemoryBlock(firstUser.account.derive2Pubkey)),
).toBeTruthy()
expect(body).toMatchObject({
type: CrossGroupType_OUTBOUND,
otherGroup: foreignTopic,
transfer: {
sender: {
amount: '100',
},
},
})
})
it('cross group recv transaction', async () => {
const crossGroupRecvTransactionDraft = new TransactionDraft()
crossGroupRecvTransactionDraft.amount = new Decimal('100')
crossGroupRecvTransactionDraft.backendTransactionId = 5
crossGroupRecvTransactionDraft.createdAt = new Date().toISOString()
crossGroupRecvTransactionDraft.linkedUser = firstUser.identifier
crossGroupRecvTransactionDraft.user = foreignUser.identifier
crossGroupRecvTransactionDraft.type = InputTransactionType.RECEIVE
const context = new CreateTransactionRecipeContext(crossGroupRecvTransactionDraft)
await context.run()
const transaction = context.getTransactionRecipe()
// console.log(new TransactionLoggingView(transaction))
expect(transaction).toMatchObject({
type: TransactionType.GRADIDO_TRANSFER,
protocolVersion: '3.3',
community: {
rootPubkey: foreignKeyPair.publicKey,
foreign: 1,
iotaTopic: foreignTopic,
},
otherCommunity: {
rootPubkey: keyPair.publicKey,
foreign: 0,
iotaTopic: topic,
},
signingAccount: {
derive2Pubkey: firstUser.account.derive2Pubkey,
},
recipientAccount: {
derive2Pubkey: foreignUser.account.derive2Pubkey,
},
amount: new Decimal(100),
backendTransactions: [
{
typeId: InputTransactionType.RECEIVE,
},
],
})
const deserializer = new InteractionDeserialize(new MemoryBlock(transaction.bodyBytes))
deserializer.run()
const body = deserializer.getTransactionBody()
expect(body).not.toBeNull()
// console.log(new TransactionBodyLoggingView(body))
expect(body?.isTransfer()).toBeTruthy()
const transfer = body?.getTransfer()
expect(transfer).not.toBeNull()
expect(
transfer?.getRecipient()?.equal(new MemoryBlock(foreignUser.account.derive2Pubkey)),
).toBeTruthy()
expect(
transfer?.getSender().getPubkey()?.equal(new MemoryBlock(firstUser.account.derive2Pubkey)),
).toBeTruthy()
expect(body).toMatchObject({
type: CrossGroupType_INBOUND,
otherGroup: topic,
transfer: {
sender: {
amount: '100',
},
},
})
})
})

View File

@ -1,89 +0,0 @@
import { Account } from '@entity/Account'
import { Community } from '@entity/Community'
import { Transaction } from '@entity/Transaction'
import { InputTransactionType } from '@/graphql/enum/InputTransactionType'
import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType'
import { CommunityDraft } from '@/graphql/input/CommunityDraft'
import { TransactionDraft } from '@/graphql/input/TransactionDraft'
import { UserAccountDraft } from '@/graphql/input/UserAccountDraft'
import { TransactionError } from '@/graphql/model/TransactionError'
import { AbstractTransactionRole } from './AbstractTransaction.role'
import { AbstractTransactionRecipeRole } from './AbstractTransactionRecipeRole'
import { BalanceChangingTransactionRecipeRole } from './BalanceChangingTransactionRecipeRole'
import { CommunityRootTransactionRole } from './CommunityRootTransaction.role'
import { CreationTransactionRole } from './CreationTransaction.role'
import { RegisterAddressTransactionRole } from './RegisterAddressTransaction.role'
import { SendTransactionRole } from './SendTransaction.role'
/**
* @DCI-Context
* Context for create and add Transaction Recipe to DB
*/
export interface AdditionalData {
community?: Community
account?: Account
}
export class CreateTransactionRecipeContext {
private transactionRecipe: AbstractTransactionRecipeRole
// eslint-disable-next-line no-useless-constructor
public constructor(
private draft: CommunityDraft | TransactionDraft | UserAccountDraft,
private data?: AdditionalData,
) {}
public getTransactionRecipe(): Transaction {
return this.transactionRecipe.getTransaction()
}
/**
* @returns true if a transaction recipe was created and false if it wasn't necessary
*/
public async run(): Promise<boolean> {
if (this.draft instanceof TransactionDraft) {
// contain logic for translation from backend to dlt-connector format
let transactionTypeRole: AbstractTransactionRole
switch (this.draft.type) {
case InputTransactionType.CREATION:
transactionTypeRole = new CreationTransactionRole(this.draft)
break
case InputTransactionType.SEND:
transactionTypeRole = new SendTransactionRole(this.draft)
break
case InputTransactionType.RECEIVE:
return false
}
this.transactionRecipe = await new BalanceChangingTransactionRecipeRole().create(
this.draft,
transactionTypeRole,
)
return true
} else if (this.draft instanceof CommunityDraft) {
if (!this.data?.community) {
throw new TransactionError(TransactionErrorType.MISSING_PARAMETER, 'community was not set')
}
this.transactionRecipe = new CommunityRootTransactionRole().create(
this.draft,
this.data.community,
)
return true
} else if (this.draft instanceof UserAccountDraft) {
if (!this.data?.account) {
throw new TransactionError(TransactionErrorType.MISSING_PARAMETER, 'account was not set')
}
if (!this.data.community) {
throw new TransactionError(TransactionErrorType.MISSING_PARAMETER, 'community was not set')
}
this.transactionRecipe = await new RegisterAddressTransactionRole().create(
this.draft,
this.data.account,
this.data.community,
)
return true
}
return false
}
}

View File

@ -1,64 +0,0 @@
/* eslint-disable camelcase */
import { Community } from '@entity/Community'
import { MemoryBlock, GradidoTransactionBuilder, TransferAmount } from 'gradido-blockchain-js'
import { CommunityRepository } from '@/data/Community.repository'
import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType'
import { UserIdentifier } from '@/graphql/input/UserIdentifier'
import { TransactionError } from '@/graphql/model/TransactionError'
import { logger } from '@/logging/logger'
import { UserIdentifierLoggingView } from '@/logging/UserIdentifierLogging.view'
import { AbstractTransactionRole } from './AbstractTransaction.role'
export class CreationTransactionRole extends AbstractTransactionRole {
public getSigningUser(): UserIdentifier {
return this.self.linkedUser
}
public getRecipientUser(): UserIdentifier {
return this.self.user
}
public async getGradidoTransactionBuilder(): Promise<GradidoTransactionBuilder> {
const builder = new GradidoTransactionBuilder()
const recipientUser = await this.loadUser(this.self.user)
if (!this.self.targetDate) {
throw new TransactionError(
TransactionErrorType.MISSING_PARAMETER,
'missing targetDate for contribution',
)
}
return builder
.setTransactionCreation(
new TransferAmount(
new MemoryBlock(recipientUser.derive2Pubkey),
this.self.amount.toString(),
),
new Date(this.self.targetDate),
)
.setMemo('dummy memo for creation')
}
public async getCommunity(): Promise<Community> {
if (this.self.user.communityUuid !== this.self.linkedUser.communityUuid) {
throw new TransactionError(
TransactionErrorType.LOGIC_ERROR,
'mismatch community uuids on contribution',
)
}
const community = await CommunityRepository.getCommunityForUserIdentifier(this.self.user)
if (!community) {
logger.error(
'missing community for user identifier',
new UserIdentifierLoggingView(this.self.user),
)
throw new TransactionError(TransactionErrorType.NOT_FOUND, "couldn't find community for user")
}
return community
}
public async getOtherCommunity(): Promise<Community | null> {
return null
}
}

View File

@ -1,52 +0,0 @@
import { Account } from '@entity/Account'
import { Community } from '@entity/Community'
import {
// eslint-disable-next-line camelcase
AddressType_COMMUNITY_HUMAN,
MemoryBlock,
GradidoTransactionBuilder,
} from 'gradido-blockchain-js'
import { AccountLogic } from '@/data/Account.logic'
import { CommunityRepository } from '@/data/Community.repository'
import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType'
import { UserAccountDraft } from '@/graphql/input/UserAccountDraft'
import { TransactionError } from '@/graphql/model/TransactionError'
import { AbstractTransactionRecipeRole } from './AbstractTransactionRecipeRole'
export class RegisterAddressTransactionRole extends AbstractTransactionRecipeRole {
async create(
userAccountDraft: UserAccountDraft,
account: Account,
community: Community,
): Promise<RegisterAddressTransactionRole> {
const user = account.user
if (!user) {
throw new TransactionError(TransactionErrorType.MISSING_PARAMETER, 'missing user for account')
}
const gradidoTransactionBuilder = new GradidoTransactionBuilder()
const communityKeyPair = await CommunityRepository.loadHomeCommunityKeyPair()
const signingKeyPair = new AccountLogic(account).calculateKeyPair(communityKeyPair)
if (!signingKeyPair) {
throw new TransactionError(TransactionErrorType.NOT_FOUND, "couldn't found signing key pair")
}
const transaction = gradidoTransactionBuilder
.setRegisterAddress(
new MemoryBlock(user.derive1Pubkey),
AddressType_COMMUNITY_HUMAN,
null,
new MemoryBlock(account.derive2Pubkey),
)
.setCreatedAt(new Date(userAccountDraft.createdAt))
.sign(signingKeyPair.keyPair)
.sign(communityKeyPair.keyPair)
.build()
this.transactionBuilder
.fromGradidoTransaction(transaction)
.setCommunity(community)
.setSigningAccount(account)
return this
}
}

View File

@ -1,35 +0,0 @@
/* eslint-disable camelcase */
import {
CrossGroupType,
CrossGroupType_LOCAL,
CrossGroupType_OUTBOUND,
MemoryBlock,
GradidoTransactionBuilder,
TransferAmount,
} from 'gradido-blockchain-js'
import { UserIdentifier } from '@/graphql/input/UserIdentifier'
import { AbstractTransactionRole } from './AbstractTransaction.role'
export class SendTransactionRole extends AbstractTransactionRole {
public getSigningUser(): UserIdentifier {
return this.self.user
}
public getRecipientUser(): UserIdentifier {
return this.self.linkedUser
}
public async getGradidoTransactionBuilder(): Promise<GradidoTransactionBuilder> {
const builder = new GradidoTransactionBuilder()
const signingUser = await this.loadUser(this.self.user)
const recipientUser = await this.loadUser(this.self.linkedUser)
return builder
.setTransactionTransfer(
new TransferAmount(new MemoryBlock(signingUser.derive2Pubkey), this.self.amount.toString()),
new MemoryBlock(recipientUser.derive2Pubkey),
)
.setMemo('dummy memo for transfer')
}
}

View File

@ -1,9 +1,9 @@
/* eslint-disable camelcase */
import { AddressType_COMMUNITY_HUMAN, GradidoTransactionBuilder } from 'gradido-blockchain-js'
import { GradidoTransactionBuilder } from 'gradido-blockchain-js'
import { UserAccountDraft } from '@/graphql/input/UserAccountDraft'
import { LogError } from '@/server/LogError'
import { uuid4ToHash } from '@/utils/typeConverter'
import { accountTypeToAddressType, uuid4ToHash } from '@/utils/typeConverter'
import { KeyPairCalculation } from '../keyPairCalculation/KeyPairCalculation.context'
@ -30,7 +30,7 @@ export class RegisterAddressTransactionRole extends AbstractTransactionRole {
.setCreatedAt(new Date(this.self.createdAt))
.setRegisterAddress(
accountKeyPair.getPublicKey(),
AddressType_COMMUNITY_HUMAN,
accountTypeToAddressType(this.self.accountType),
uuid4ToHash(this.self.user.uuid),
)
.sign(communityKeyPair)

View File

@ -1,106 +0,0 @@
/* eslint-disable camelcase */
import { Transaction } from '@entity/Transaction'
import {
GradidoTransaction,
GradidoTransactionBuilder,
InteractionSerialize,
InteractionValidate,
MemoryBlock,
TransactionType_COMMUNITY_ROOT,
ValidateType_SINGLE,
} from 'gradido-blockchain-js'
import { sendMessage as iotaSendMessage } from '@/client/IotaClient'
import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType'
import { TransactionError } from '@/graphql/model/TransactionError'
import { logger } from '@/logging/logger'
export abstract class AbstractTransactionRecipeRole {
// eslint-disable-next-line no-useless-constructor
public constructor(protected self: Transaction) {}
public abstract transmitToIota(): Promise<Transaction>
public abstract getCrossGroupTypeName(): string
public validate(transactionBuilder: GradidoTransactionBuilder): GradidoTransaction {
const transaction = transactionBuilder.build()
try {
// throw an exception when something is wrong
const validator = new InteractionValidate(transaction)
validator.run(ValidateType_SINGLE)
} catch (e) {
if (e instanceof Error) {
throw new TransactionError(TransactionErrorType.VALIDATION_ERROR, e.message)
} else if (typeof e === 'string') {
throw new TransactionError(TransactionErrorType.VALIDATION_ERROR, e)
} else {
throw e
}
}
return transaction
}
protected getGradidoTransactionBuilder(): GradidoTransactionBuilder {
if (!this.self.signature) {
throw new TransactionError(
TransactionErrorType.MISSING_PARAMETER,
'missing signature in transaction recipe',
)
}
let publicKey: Buffer | undefined
if (this.self.type === TransactionType_COMMUNITY_ROOT) {
publicKey = this.self.community.rootPubkey
if (!publicKey) {
throw new TransactionError(
TransactionErrorType.MISSING_PARAMETER,
'missing community public key for community root transaction',
)
}
} else if (this.self.signingAccount) {
publicKey = this.self.signingAccount.derive2Pubkey
if (!publicKey) {
throw new TransactionError(
TransactionErrorType.MISSING_PARAMETER,
'missing signing account public key for transaction',
)
}
} else {
throw new TransactionError(
TransactionErrorType.NOT_FOUND,
"signingAccount not exist and it isn't a community root transaction",
)
}
return new GradidoTransactionBuilder()
.setTransactionBody(new MemoryBlock(this.self.bodyBytes))
.addSignaturePair(new MemoryBlock(publicKey), new MemoryBlock(this.self.signature))
}
/**
*
* @param gradidoTransaction
* @param topic
* @return iota message id
*/
protected async sendViaIota(
gradidoTransaction: GradidoTransaction,
topic: string,
): Promise<Buffer> {
// protobuf serializing function
const serialized = new InteractionSerialize(gradidoTransaction).run()
if (!serialized) {
throw new TransactionError(
TransactionErrorType.PROTO_ENCODE_ERROR,
'cannot serialize transaction',
)
}
const resultMessage = await iotaSendMessage(
Uint8Array.from(serialized.data()),
Uint8Array.from(Buffer.from(topic, 'hex')),
)
logger.info('transmitted Gradido Transaction to Iota', {
id: this.self.id,
messageId: resultMessage.messageId,
})
return Buffer.from(resultMessage.messageId, 'hex')
}
}

View File

@ -1,44 +0,0 @@
import { Transaction } from '@entity/Transaction'
import { MemoryBlock } from 'gradido-blockchain-js'
import { logger } from '@/logging/logger'
import { TransactionLoggingView } from '@/logging/TransactionLogging.view'
import { LogError } from '@/server/LogError'
import { AbstractTransactionRecipeRole } from './AbstractTransactionRecipe.role'
/**
* Inbound Transaction on recipient community, mark the gradidos as received from another community
* need to set gradido id from OUTBOUND transaction!
*/
export class InboundTransactionRecipeRole extends AbstractTransactionRecipeRole {
public getCrossGroupTypeName(): string {
return 'INBOUND'
}
public async transmitToIota(): Promise<Transaction> {
logger.debug('transmit INBOUND transaction to iota', new TransactionLoggingView(this.self))
const builder = this.getGradidoTransactionBuilder()
const pairingTransaction = await new TransactionLogic(this.self).findPairTransaction()
if (!pairingTransaction.iotaMessageId || pairingTransaction.iotaMessageId.length !== 32) {
throw new LogError(
'missing iota message id in pairing transaction, was it already send?',
new TransactionLoggingView(pairingTransaction),
)
}
builder.setParentMessageId(new MemoryBlock(pairingTransaction.iotaMessageId))
this.self.pairingTransactionId = pairingTransaction.id
this.self.pairingTransaction = pairingTransaction
pairingTransaction.pairingTransactionId = this.self.id
if (!this.self.otherCommunity) {
throw new LogError('missing other community')
}
this.self.iotaMessageId = await this.sendViaIota(
this.validate(builder),
this.self.otherCommunity.iotaTopic,
)
return this.self
}
}

View File

@ -1,24 +0,0 @@
import { Transaction } from '@entity/Transaction'
import { logger } from '@/logging/logger'
import { TransactionLoggingView } from '@/logging/TransactionLogging.view'
import { AbstractTransactionRecipeRole } from './AbstractTransactionRecipe.role'
export class LocalTransactionRecipeRole extends AbstractTransactionRecipeRole {
public getCrossGroupTypeName(): string {
return 'LOCAL'
}
public async transmitToIota(): Promise<Transaction> {
logger.debug(
`transmit ${this.getCrossGroupTypeName()} transaction to iota`,
new TransactionLoggingView(this.self),
)
this.self.iotaMessageId = await this.sendViaIota(
this.validate(this.getGradidoTransactionBuilder()),
this.self.community.iotaTopic,
)
return this.self
}
}

View File

@ -1,10 +0,0 @@
import { LocalTransactionRecipeRole } from './LocalTransactionRecipe.role'
/**
* Outbound Transaction on sender community, mark the gradidos as sended out of community
*/
export class OutboundTransactionRecipeRole extends LocalTransactionRecipeRole {
public getCrossGroupTypeName(): string {
return 'OUTBOUND'
}
}

View File

@ -1,172 +0,0 @@
import 'reflect-metadata'
import { Account } from '@entity/Account'
import { Decimal } from 'decimal.js-light'
import { CrossGroupType_INBOUND, CrossGroupType_OUTBOUND, InteractionDeserialize, InteractionToJson, InteractionValidate, MemoryBlock } from 'gradido-blockchain-js'
import { v4 } from 'uuid'
import { TestDB } from '@test/TestDB'
import { CONFIG } from '@/config'
import { KeyPair } from '@/data/KeyPair'
import { Mnemonic } from '@/data/Mnemonic'
import { InputTransactionType } from '@/graphql/enum/InputTransactionType'
import { TransactionDraft } from '@/graphql/input/TransactionDraft'
import { logger } from '@/logging/logger'
import { CreateTransactionRecipeContext } from '../backendToDb/transaction/CreateTransactionRecipe.context'
import { TransmitToIotaContext } from './TransmitToIota.context'
// eslint-disable-next-line import/order
import { communitySeed } from '@test/seeding/Community.seed'
// eslint-disable-next-line import/order
import { createUserSet, UserSet } from '@test/seeding/UserSet.seed'
jest.mock('@typeorm/DataSource', () => ({
getDataSource: jest.fn(() => TestDB.instance.dbConnect),
}))
jest.mock('@/client/IotaClient', () => {
return {
sendMessage: jest.fn().mockReturnValue({
messageId: '5498130bc3918e1a7143969ce05805502417e3e1bd596d3c44d6a0adeea22710',
}),
}
})
CONFIG.IOTA_HOME_COMMUNITY_SEED = '034b0229a2ba4e98e1cc5e8767dca886279b484303ffa73546bd5f5bf0b71285'
const homeCommunityUuid = v4()
const foreignCommunityUuid = v4()
const keyPair = new KeyPair(new Mnemonic(CONFIG.IOTA_HOME_COMMUNITY_SEED))
const foreignKeyPair = new KeyPair(
new Mnemonic('5d4e163c078cc6b51f5c88f8422bc8f21d1d59a284515ab1ea79e1c176ebec50'),
)
let moderator: UserSet
let firstUser: UserSet
let secondUser: UserSet
let foreignUser: UserSet
const now = new Date()
describe('interactions/transmitToIota/TransmitToIotaContext', () => {
beforeAll(async () => {
await TestDB.instance.setupTestDB()
await communitySeed(homeCommunityUuid, false)
await communitySeed(foreignCommunityUuid, true, foreignKeyPair)
moderator = createUserSet(homeCommunityUuid, keyPair)
firstUser = createUserSet(homeCommunityUuid, keyPair)
secondUser = createUserSet(homeCommunityUuid, keyPair)
foreignUser = createUserSet(foreignCommunityUuid, foreignKeyPair)
await Account.save([
moderator.account,
firstUser.account,
secondUser.account,
foreignUser.account,
])
})
afterAll(async () => {
await TestDB.instance.teardownTestDB()
})
it('LOCAL transaction', async () => {
const creationTransactionDraft = new TransactionDraft()
creationTransactionDraft.amount = new Decimal('1000')
creationTransactionDraft.backendTransactionId = 1
creationTransactionDraft.createdAt = new Date().toISOString()
creationTransactionDraft.linkedUser = moderator.identifier
creationTransactionDraft.user = firstUser.identifier
creationTransactionDraft.type = InputTransactionType.CREATION
creationTransactionDraft.targetDate = new Date().toISOString()
const transactionRecipeContext = new CreateTransactionRecipeContext(creationTransactionDraft)
await transactionRecipeContext.run()
const transaction = transactionRecipeContext.getTransactionRecipe()
const context = new TransmitToIotaContext(transaction)
const debugSpy = jest.spyOn(logger, 'debug')
await context.run()
expect(
transaction.iotaMessageId?.compare(
Buffer.from('5498130bc3918e1a7143969ce05805502417e3e1bd596d3c44d6a0adeea22710', 'hex'),
),
).toBe(0)
expect(debugSpy).toHaveBeenNthCalledWith(
3,
expect.stringContaining('transmit LOCAL transaction to iota'),
expect.objectContaining({}),
)
})
it('OUTBOUND transaction', async () => {
const crossGroupSendTransactionDraft = new TransactionDraft()
crossGroupSendTransactionDraft.amount = new Decimal('100')
crossGroupSendTransactionDraft.backendTransactionId = 4
crossGroupSendTransactionDraft.createdAt = now.toISOString()
crossGroupSendTransactionDraft.linkedUser = foreignUser.identifier
crossGroupSendTransactionDraft.user = firstUser.identifier
crossGroupSendTransactionDraft.type = InputTransactionType.SEND
const transactionRecipeContext = new CreateTransactionRecipeContext(
crossGroupSendTransactionDraft,
)
await transactionRecipeContext.run()
const transaction = transactionRecipeContext.getTransactionRecipe()
await transaction.save()
const deserializer = new InteractionDeserialize(new MemoryBlock(transaction.bodyBytes))
deserializer.run()
const body = deserializer.getTransactionBody()
expect(body).not.toBeNull()
expect(body?.getType()).toEqual(CrossGroupType_OUTBOUND)
const context = new TransmitToIotaContext(transaction)
const debugSpy = jest.spyOn(logger, 'debug')
await context.run()
expect(
transaction.iotaMessageId?.compare(
Buffer.from('5498130bc3918e1a7143969ce05805502417e3e1bd596d3c44d6a0adeea22710', 'hex'),
),
).toBe(0)
expect(debugSpy).toHaveBeenNthCalledWith(
5,
expect.stringContaining('transmit OUTBOUND transaction to iota'),
expect.objectContaining({}),
)
})
it('INBOUND transaction', async () => {
const crossGroupRecvTransactionDraft = new TransactionDraft()
crossGroupRecvTransactionDraft.amount = new Decimal('100')
crossGroupRecvTransactionDraft.backendTransactionId = 5
crossGroupRecvTransactionDraft.createdAt = now.toISOString()
crossGroupRecvTransactionDraft.linkedUser = firstUser.identifier
crossGroupRecvTransactionDraft.user = foreignUser.identifier
crossGroupRecvTransactionDraft.type = InputTransactionType.RECEIVE
const transactionRecipeContext = new CreateTransactionRecipeContext(
crossGroupRecvTransactionDraft,
)
await transactionRecipeContext.run()
const transaction = transactionRecipeContext.getTransactionRecipe()
await transaction.save()
// console.log(new TransactionLoggingView(transaction))
const deserializer = new InteractionDeserialize(new MemoryBlock(transaction.bodyBytes))
deserializer.run()
const body = deserializer.getTransactionBody()
expect(body?.getType()).toEqual(CrossGroupType_INBOUND)
const context = new TransmitToIotaContext(transaction)
const debugSpy = jest.spyOn(logger, 'debug')
await context.run()
expect(
transaction.iotaMessageId?.compare(
Buffer.from('5498130bc3918e1a7143969ce05805502417e3e1bd596d3c44d6a0adeea22710', 'hex'),
),
).toBe(0)
expect(debugSpy).toHaveBeenNthCalledWith(
7,
expect.stringContaining('transmit INBOUND transaction to iota'),
expect.objectContaining({}),
)
})
})

View File

@ -1,73 +0,0 @@
/* eslint-disable camelcase */
import { Transaction } from '@entity/Transaction'
import {
CrossGroupType_INBOUND,
CrossGroupType_LOCAL,
CrossGroupType_OUTBOUND,
InteractionDeserialize,
MemoryBlock,
} from 'gradido-blockchain-js'
import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType'
import { TransactionError } from '@/graphql/model/TransactionError'
import { logger } from '@/logging/logger'
import { TransactionLoggingView } from '@/logging/TransactionLogging.view'
import { LogError } from '@/server/LogError'
import { getDataSource } from '@/typeorm/DataSource'
import { AbstractTransactionRecipeRole } from './AbstractTransactionRecipe.role'
import { InboundTransactionRecipeRole } from './InboundTransactionRecipe.role'
import { LocalTransactionRecipeRole } from './LocalTransactionRecipe.role'
import { OutboundTransactionRecipeRole } from './OutboundTransactionRecipeRole'
/**
* @DCI-Context
* Context for sending transaction recipe to iota
* send every transaction only once to iota!
*/
export class TransmitToIotaContext {
private transactionRecipeRole: AbstractTransactionRecipeRole
public constructor(transaction: Transaction) {
const deserializer = new InteractionDeserialize(new MemoryBlock(transaction.bodyBytes))
deserializer.run()
const transactionBody = deserializer.getTransactionBody()
if (!transactionBody) {
throw new TransactionError(
TransactionErrorType.PROTO_DECODE_ERROR,
'error decoding body bytes',
)
}
switch (transactionBody.getType()) {
case CrossGroupType_LOCAL:
this.transactionRecipeRole = new LocalTransactionRecipeRole(transaction)
break
case CrossGroupType_INBOUND:
this.transactionRecipeRole = new InboundTransactionRecipeRole(transaction)
break
case CrossGroupType_OUTBOUND:
this.transactionRecipeRole = new OutboundTransactionRecipeRole(transaction)
break
default:
throw new LogError('unknown cross group type', transactionBody.getType())
}
}
public async run(): Promise<void> {
const transaction = await this.transactionRecipeRole.transmitToIota()
logger.debug('transaction sended via iota', new TransactionLoggingView(transaction))
// store changes in db
// prevent endless loop
const pairingTransaction = transaction.pairingTransaction
if (pairingTransaction) {
transaction.pairingTransaction = undefined
await getDataSource().transaction(async (transactionalEntityManager) => {
await transactionalEntityManager.save(transaction)
await transactionalEntityManager.save(pairingTransaction)
})
} else {
await transaction.save()
}
logger.info('sended transaction successfully updated in db')
}
}

View File

@ -1,6 +1,5 @@
import util from 'util'
import { Decimal } from 'decimal.js-light'
import { Timestamp, TimestampSeconds } from 'gradido-blockchain-js'
export abstract class AbstractLoggingView {
@ -25,13 +24,6 @@ export abstract class AbstractLoggingView {
return undefined
}
protected decimalToString(number: Decimal | undefined | null): string | undefined {
if (number) {
return number.toString()
}
return undefined
}
protected timestampSecondsToDateString(timestamp: TimestampSeconds): string | undefined {
if (timestamp && timestamp.getSeconds()) {
return timestamp.getDate().toISOString()

View File

@ -1,32 +0,0 @@
import { Account } from '@entity/Account'
import { addressTypeToString } from 'gradido-blockchain-js'
import { AccountType } from '@/graphql/enum/AccountType'
import { accountTypeToAddressType } from '@/utils/typeConverter'
import { AbstractLoggingView } from './AbstractLogging.view'
import { UserLoggingView } from './UserLogging.view'
export class AccountLoggingView extends AbstractLoggingView {
public constructor(private account: Account) {
super()
}
public toJSON() {
return {
id: this.account.id,
user: this.account.user ? new UserLoggingView(this.account.user).toJSON() : null,
derivationIndex: this.account.derivationIndex,
derive2Pubkey: this.account.derive2Pubkey.toString(this.bufferStringFormat),
type: addressTypeToString(
accountTypeToAddressType(this.account.type as unknown as AccountType),
),
createdAt: this.dateToString(this.account.createdAt),
confirmedAt: this.dateToString(this.account.confirmedAt),
balanceOnConfirmation: this.decimalToString(this.account.balanceOnConfirmation),
balanceConfirmedAt: this.dateToString(this.account.balanceConfirmedAt),
balanceOnCreation: this.decimalToString(this.account.balanceOnCreation),
balanceCreatedAt: this.dateToString(this.account.balanceCreatedAt),
}
}
}

View File

@ -1,30 +0,0 @@
import { BackendTransaction } from '@entity/BackendTransaction'
import { InputTransactionType } from '@/graphql/enum/InputTransactionType'
import { getEnumValue } from '@/utils/typeConverter'
import { AbstractLoggingView } from './AbstractLogging.view'
import { TransactionLoggingView } from './TransactionLogging.view'
export class BackendTransactionLoggingView extends AbstractLoggingView {
public constructor(private self: BackendTransaction) {
super()
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public toJSON(showTransaction = true): any {
return {
id: this.self.id,
backendTransactionId: this.self.backendTransactionId,
transaction:
showTransaction && this.self.transaction
? new TransactionLoggingView(this.self.transaction).toJSON(false)
: undefined,
type: getEnumValue(InputTransactionType, this.self.typeId),
balance: this.decimalToString(this.self.balance),
createdAt: this.dateToString(this.self.createdAt),
confirmedAt: this.dateToString(this.self.confirmedAt),
verifiedOnBackend: this.self.verifiedOnBackend,
}
}
}

View File

@ -1,24 +0,0 @@
import { Community } from '@entity/Community'
import { AbstractLoggingView } from './AbstractLogging.view'
import { AccountLoggingView } from './AccountLogging.view'
export class CommunityLoggingView extends AbstractLoggingView {
public constructor(private self: Community) {
super()
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public toJSON(): any {
return {
id: this.self.id,
iotaTopic: this.self.iotaTopic,
foreign: this.self.foreign,
publicKey: this.self.rootPubkey?.toString(this.bufferStringFormat),
createdAt: this.dateToString(this.self.createdAt),
confirmedAt: this.dateToString(this.self.confirmedAt),
aufAccount: this.self.aufAccount ? new AccountLoggingView(this.self.aufAccount) : undefined,
gmwAccount: this.self.gmwAccount ? new AccountLoggingView(this.self.gmwAccount) : undefined,
}
}
}

View File

@ -16,7 +16,7 @@ export class TransactionDraftLoggingView extends AbstractLoggingView {
user: new UserIdentifierLoggingView(this.self.user).toJSON(),
linkedUser: new UserIdentifierLoggingView(this.self.linkedUser).toJSON(),
backendTransactionId: this.self.backendTransactionId,
amount: this.decimalToString(this.self.amount),
amount: Number(this.self.amount),
type: getEnumValue(InputTransactionType, this.self.type),
createdAt: this.self.createdAt,
targetDate: this.self.targetDate,

View File

@ -1,66 +0,0 @@
import { Transaction } from '@entity/Transaction'
import { TransactionType } from '@/data/proto/3_3/enum/TransactionType'
import { LogError } from '@/server/LogError'
import { getEnumValue } from '@/utils/typeConverter'
import { AbstractLoggingView } from './AbstractLogging.view'
import { AccountLoggingView } from './AccountLogging.view'
import { BackendTransactionLoggingView } from './BackendTransactionLogging.view'
import { CommunityLoggingView } from './CommunityLogging.view'
export class TransactionLoggingView extends AbstractLoggingView {
public constructor(private self: Transaction) {
super()
if (this.self.community === undefined) {
throw new LogError('sender community is zero')
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public toJSON(showBackendTransactions = true, deep = 1): any {
return {
id: this.self.id,
nr: this.self.nr,
bodyBytesLength: this.self.bodyBytes.length,
createdAt: this.dateToString(this.self.createdAt),
confirmedAt: this.dateToString(this.self.confirmedAt),
protocolVersion: this.self.protocolVersion,
type: getEnumValue(TransactionType, this.self.type),
signature: this.self.signature.subarray(0, 31).toString(this.bufferStringFormat) + '..',
community: new CommunityLoggingView(this.self.community).toJSON(),
otherCommunity: this.self.otherCommunity
? new CommunityLoggingView(this.self.otherCommunity)
: { id: this.self.otherCommunityId },
iotaMessageId: this.self.iotaMessageId
? this.self.iotaMessageId.toString(this.bufferStringFormat)
: undefined,
signingAccount: this.self.signingAccount
? new AccountLoggingView(this.self.signingAccount)
: { id: this.self.signingAccountId },
recipientAccount: this.self.recipientAccount
? new AccountLoggingView(this.self.recipientAccount)
: { id: this.self.recipientAccountId },
pairingTransaction:
this.self.pairingTransaction && deep === 1
? new TransactionLoggingView(this.self.pairingTransaction).toJSON(
showBackendTransactions,
deep + 1,
)
: { id: this.self.pairingTransaction },
amount: this.decimalToString(this.self.amount),
accountBalanceOnCreation: this.decimalToString(this.self.accountBalanceOnCreation),
accountBalanceOnConfirmation: this.decimalToString(this.self.accountBalanceOnConfirmation),
runningHash: this.self.runningHash
? this.self.runningHash.toString(this.bufferStringFormat)
: undefined,
iotaMilestone: this.self.iotaMilestone,
backendTransactions:
showBackendTransactions && this.self.backendTransactions
? this.self.backendTransactions.map((backendTransaction) =>
new BackendTransactionLoggingView(backendTransaction).toJSON(false),
)
: undefined,
}
}
}

View File

@ -1,20 +0,0 @@
import { User } from '@entity/User'
import { AbstractLoggingView } from './AbstractLogging.view'
export class UserLoggingView extends AbstractLoggingView {
public constructor(private user: User) {
super()
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public toJSON(): any {
return {
id: this.user.id,
gradidoId: this.user.gradidoID,
derive1Pubkey: this.user.derive1Pubkey.toString(this.bufferStringFormat),
createdAt: this.dateToString(this.user.createdAt),
confirmedAt: this.dateToString(this.user.confirmedAt),
}
}
}

View File

@ -1,63 +0,0 @@
import { LogError } from '@/server/LogError'
import { InterruptiveSleep } from '../utils/InterruptiveSleep'
// Source: https://refactoring.guru/design-patterns/singleton/typescript/example
// and ../federation/client/FederationClientFactory.ts
/**
* A Singleton class defines the `getInstance` method that lets clients access
* the unique singleton instance.
*/
// eslint-disable-next-line @typescript-eslint/no-extraneous-class
export class InterruptiveSleepManager {
// eslint-disable-next-line no-use-before-define
private static instance: InterruptiveSleepManager
private interruptiveSleep: Map<string, InterruptiveSleep> = new Map<string, InterruptiveSleep>()
private stepSizeMilliseconds = 10
/**
* 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(): InterruptiveSleepManager {
if (!InterruptiveSleepManager.instance) {
InterruptiveSleepManager.instance = new InterruptiveSleepManager()
}
return InterruptiveSleepManager.instance
}
/**
* only for new created InterruptiveSleepManager Entries!
* @param step size in ms in which new! InterruptiveSleepManager check if they where triggered
*/
public setStepSize(ms: number) {
this.stepSizeMilliseconds = ms
}
public interrupt(key: string): void {
const interruptiveSleep = this.interruptiveSleep.get(key)
if (interruptiveSleep) {
interruptiveSleep.interrupt()
}
}
public sleep(key: string, ms: number): Promise<void> {
if (!this.interruptiveSleep.has(key)) {
this.interruptiveSleep.set(key, new InterruptiveSleep(this.stepSizeMilliseconds))
}
const interruptiveSleep = this.interruptiveSleep.get(key)
if (!interruptiveSleep) {
throw new LogError('map entry not exist after setting it')
}
return interruptiveSleep.sleep(ms)
}
}

View File

@ -1,49 +0,0 @@
import { TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY } from '@/data/const'
import { TransactionRepository } from '@/data/Transaction.repository'
import { TransmitToIotaContext } from '@/interactions/transmitToIota/TransmitToIota.context'
import { InterruptiveSleepManager } from '@/manager/InterruptiveSleepManager'
import { logger } from '../logging/logger'
function sleep(ms: number) {
return new Promise((resolve) => {
setTimeout(resolve, ms)
})
}
let running = true
export const stopTransmitToIota = (): void => {
running = false
}
/**
* check for pending transactions:
* - if one found call TransmitToIotaContext
* - if not, wait 1000 ms and try again
* if a new transaction was added, the sleep will be interrupted
*/
export const transmitToIota = async (): Promise<void> => {
logger.info('start iota message transmitter')
// eslint-disable-next-line no-unmodified-loop-condition
while (running) {
try {
while (true) {
const recipe = await TransactionRepository.getNextPendingTransaction()
if (!recipe) break
const transmitToIotaContext = new TransmitToIotaContext(recipe)
await transmitToIotaContext.run()
}
await InterruptiveSleepManager.getInstance().sleep(
TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY,
1000,
)
} catch (error) {
logger.error('error while transmitting to iota, retry in 10 seconds ', error)
await sleep(10000)
}
}
logger.error(
'end iota message transmitter, no further transaction will be transmitted. !!! Please restart Server !!!',
)
}

View File

@ -1,31 +0,0 @@
/**
* Sleep, that can be interrupted
* call sleep only for msSteps and than check if interrupt was called
*/
export class InterruptiveSleep {
private interruptSleep = false
private msSteps = 10
constructor(msSteps: number) {
this.msSteps = msSteps
}
public interrupt(): void {
this.interruptSleep = true
}
private static _sleep(ms: number) {
return new Promise((resolve) => {
setTimeout(resolve, ms)
})
}
public async sleep(ms: number): Promise<void> {
let waited = 0
this.interruptSleep = false
while (waited < ms && !this.interruptSleep) {
await InterruptiveSleep._sleep(this.msSteps)
waited += this.msSteps
}
}
}