Merge branch 'master' into admin_update_community_list

This commit is contained in:
einhornimmond 2024-02-22 08:39:27 +01:00 committed by GitHub
commit eba52319be
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
45 changed files with 1909 additions and 52 deletions

View File

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

View File

@ -39,7 +39,8 @@
"reflect-metadata": "^0.1.13",
"sodium-native": "^4.0.4",
"tsconfig-paths": "^4.1.2",
"type-graphql": "^2.0.0-beta.2"
"type-graphql": "^2.0.0-beta.2",
"uuid": "^9.0.1"
},
"devDependencies": {
"@eslint-community/eslint-plugin-eslint-comments": "^3.2.1",

View File

@ -50,7 +50,7 @@ jest.mock('@iota/client', () => {
describe('Iota Tests', () => {
it('test mocked sendDataMessage', async () => {
const result = await sendMessage('Test Message')
const result = await sendMessage('Test Message', 'topic')
expect(result).toBe('5498130bc3918e1a7143969ce05805502417e3e1bd596d3c44d6a0adeea22710')
})

View File

@ -2,17 +2,19 @@ import { ClientBuilder } from '@iota/client'
import { MessageWrapper } from '@iota/client/lib/types'
import { CONFIG } from '@/config'
const client = new ClientBuilder().node(CONFIG.IOTA_API_URL).build()
/**
* send data message onto iota tangle
* use CONFIG.IOTA_COMMUNITY_ALIAS for index
* @param {string | Uint8Array} message - the message as utf based string, will be converted to hex automatically from @iota/client
* @param {string | Uint8Array} topic - the iota topic to which the message will be sended
* @return {Promise<MessageWrapper>} the iota message typed
*/
function sendMessage(message: string | Uint8Array): Promise<MessageWrapper> {
return client.message().index(CONFIG.IOTA_COMMUNITY_ALIAS).data(message).submit()
function sendMessage(
message: string | Uint8Array,
topic: string | Uint8Array,
): Promise<MessageWrapper> {
return client.message().index(topic).data(message).submit()
}
/**

View File

@ -31,7 +31,7 @@ const database = {
const iota = {
IOTA_API_URL: process.env.IOTA_API_URL ?? 'https://chrysalis-nodes.iota.org',
IOTA_COMMUNITY_ALIAS: process.env.IOTA_COMMUNITY_ALIAS ?? 'GRADIDO: TestHelloWelt2',
IOTA_HOME_COMMUNITY_SEED: process.env.IOTA_HOME_COMMUNITY_SEED ?? null,
IOTA_HOME_COMMUNITY_SEED: process.env.IOTA_HOME_COMMUNITY_SEED?.substring(0, 32) ?? null,
}
const dltConnector = {

View File

@ -0,0 +1,35 @@
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

@ -6,6 +6,7 @@ import { LogError } from '@/server/LogError'
import { toPublic, derivePrivate, sign, verify, generateFromSeed } from 'bip32-ed25519'
import { Mnemonic } from './Mnemonic'
import { SignaturePair } from './proto/3_3/SignaturePair'
/**
* Class Managing Key Pair and also generate, sign and verify signature with it
@ -81,7 +82,7 @@ export class KeyPair {
return sign(message, this.getExtendPrivateKey())
}
public verify(message: Buffer, signature: Buffer): boolean {
return verify(message, signature, this.getExtendPublicKey())
public static verify(message: Buffer, { signature, pubKey }: SignaturePair): boolean {
return verify(message, signature, pubKey)
}
}

View File

@ -3,10 +3,13 @@ import { entropyToMnemonic, mnemonicToSeedSync } from 'bip39'
// eslint-disable-next-line camelcase
import { randombytes_buf } from 'sodium-native'
import { LogError } from '@/server/LogError'
export class Mnemonic {
private _passphrase = ''
public constructor(seed?: Buffer | string) {
if (seed) {
Mnemonic.validateSeed(seed)
this._passphrase = entropyToMnemonic(seed)
return
}
@ -22,4 +25,24 @@ export class Mnemonic {
public get seed(): Buffer {
return mnemonicToSeedSync(this._passphrase)
}
public static validateSeed(seed: Buffer | string): void {
let seedBuffer: Buffer
if (!Buffer.isBuffer(seed)) {
seedBuffer = Buffer.from(seed, 'hex')
} else {
seedBuffer = seed
}
if (seedBuffer.length < 16 || seedBuffer.length > 32 || seedBuffer.length % 4 !== 0) {
throw new LogError(
'invalid seed, must be in binary between 16 and 32 Bytes, Power of 4, for more infos: https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki#generating-the-mnemonic',
{
seedBufferHex: seedBuffer.toString('hex'),
toShort: seedBuffer.length < 16,
toLong: seedBuffer.length > 32,
powerOf4: seedBuffer.length % 4,
},
)
}
}
}

View File

@ -59,6 +59,10 @@ export class TransactionBuilder {
return this.transaction.community
}
public getOtherCommunity(): Community | undefined {
return this.transaction.otherCommunity
}
public setSigningAccount(signingAccount: Account): TransactionBuilder {
this.transaction.signingAccount = signingAccount
return this
@ -103,22 +107,18 @@ export class TransactionBuilder {
return this
}
public async setSenderCommunityFromSenderUser(
senderUser: UserIdentifier,
): Promise<TransactionBuilder> {
public async setCommunityFromUser(user: UserIdentifier): Promise<TransactionBuilder> {
// get sender community
const community = await CommunityRepository.getCommunityForUserIdentifier(senderUser)
const community = await CommunityRepository.getCommunityForUserIdentifier(user)
if (!community) {
throw new LogError("couldn't find community for transaction")
}
return this.setCommunity(community)
}
public async setOtherCommunityFromRecipientUser(
recipientUser: UserIdentifier,
): Promise<TransactionBuilder> {
public async setOtherCommunityFromUser(user: UserIdentifier): Promise<TransactionBuilder> {
// get recipient community
const otherCommunity = await CommunityRepository.getCommunityForUserIdentifier(recipientUser)
const otherCommunity = await CommunityRepository.getCommunityForUserIdentifier(user)
return this.setOtherCommunity(otherCommunity)
}

View File

@ -0,0 +1,323 @@
import { Community } from '@entity/Community'
import { Transaction } from '@entity/Transaction'
import { Decimal } from 'decimal.js-light'
import { logger } from '@/logging/logger'
import { CommunityRoot } from './proto/3_3/CommunityRoot'
import { CrossGroupType } from './proto/3_3/enum/CrossGroupType'
import { GradidoCreation } from './proto/3_3/GradidoCreation'
import { GradidoDeferredTransfer } from './proto/3_3/GradidoDeferredTransfer'
import { GradidoTransfer } from './proto/3_3/GradidoTransfer'
import { RegisterAddress } from './proto/3_3/RegisterAddress'
import { TransactionBody } from './proto/3_3/TransactionBody'
import { TransactionLogic } from './Transaction.logic'
let a: Transaction
let b: Transaction
describe('data/transaction.logic', () => {
describe('isBelongTogether', () => {
beforeEach(() => {
const now = new Date()
let body = new TransactionBody()
body.type = CrossGroupType.OUTBOUND
body.transfer = new GradidoTransfer()
body.otherGroup = 'recipient group'
a = new Transaction()
a.community = new Community()
a.communityId = 1
a.otherCommunityId = 2
a.id = 1
a.signingAccountId = 1
a.recipientAccountId = 2
a.createdAt = now
a.amount = new Decimal('100')
a.signature = Buffer.alloc(64)
a.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
body = new TransactionBody()
body.type = CrossGroupType.INBOUND
body.transfer = new GradidoTransfer()
body.otherGroup = 'sending group'
b = new Transaction()
b.community = new Community()
b.communityId = 2
b.otherCommunityId = 1
b.id = 2
b.signingAccountId = 1
b.recipientAccountId = 2
b.createdAt = now
b.amount = new Decimal('100')
b.signature = Buffer.alloc(64)
b.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
})
const spy = jest.spyOn(logger, 'info')
it('true', () => {
const logic = new TransactionLogic(a)
expect(logic.isBelongTogether(b)).toBe(true)
})
it('false because of same id', () => {
b.id = 1
const logic = new TransactionLogic(a)
expect(logic.isBelongTogether(b)).toBe(false)
expect(spy).toHaveBeenLastCalledWith('id is the same, it is the same transaction!')
})
it('false because of different signing accounts', () => {
b.signingAccountId = 17
const logic = new TransactionLogic(a)
expect(logic.isBelongTogether(b)).toBe(false)
expect(spy).toHaveBeenLastCalledWith(
'transaction a and b are not pairs',
expect.objectContaining({}),
)
})
it('false because of different recipient accounts', () => {
b.recipientAccountId = 21
const logic = new TransactionLogic(a)
expect(logic.isBelongTogether(b)).toBe(false)
expect(spy).toHaveBeenLastCalledWith(
'transaction a and b are not pairs',
expect.objectContaining({}),
)
})
it('false because of different community ids', () => {
b.communityId = 6
const logic = new TransactionLogic(a)
expect(logic.isBelongTogether(b)).toBe(false)
expect(spy).toHaveBeenLastCalledWith(
'transaction a and b are not pairs',
expect.objectContaining({}),
)
})
it('false because of different other community ids', () => {
b.otherCommunityId = 3
const logic = new TransactionLogic(a)
expect(logic.isBelongTogether(b)).toBe(false)
expect(spy).toHaveBeenLastCalledWith(
'transaction a and b are not pairs',
expect.objectContaining({}),
)
})
it('false because of different createdAt', () => {
b.createdAt = new Date('2021-01-01T17:12')
const logic = new TransactionLogic(a)
expect(logic.isBelongTogether(b)).toBe(false)
expect(spy).toHaveBeenLastCalledWith(
'transaction a and b are not pairs',
expect.objectContaining({}),
)
})
describe('false because of mismatching cross group type', () => {
const body = new TransactionBody()
it('a is LOCAL', () => {
body.type = CrossGroupType.LOCAL
a.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
const logic = new TransactionLogic(a)
expect(logic.isBelongTogether(b)).toBe(false)
expect(spy).toHaveBeenNthCalledWith(7, 'no one can be LOCAL')
expect(spy).toHaveBeenLastCalledWith(
"cross group types don't match",
expect.objectContaining({}),
)
})
it('b is LOCAL', () => {
body.type = CrossGroupType.LOCAL
b.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
const logic = new TransactionLogic(a)
expect(logic.isBelongTogether(b)).toBe(false)
expect(spy).toHaveBeenNthCalledWith(9, 'no one can be LOCAL')
expect(spy).toHaveBeenLastCalledWith(
"cross group types don't match",
expect.objectContaining({}),
)
})
it('both are INBOUND', () => {
body.type = CrossGroupType.INBOUND
a.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
b.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
const logic = new TransactionLogic(a)
expect(logic.isBelongTogether(b)).toBe(false)
expect(spy).toHaveBeenLastCalledWith(
"cross group types don't match",
expect.objectContaining({}),
)
})
it('both are OUTBOUND', () => {
body.type = CrossGroupType.OUTBOUND
a.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
b.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
const logic = new TransactionLogic(a)
expect(logic.isBelongTogether(b)).toBe(false)
expect(spy).toHaveBeenLastCalledWith(
"cross group types don't match",
expect.objectContaining({}),
)
})
it('a is CROSS', () => {
body.type = CrossGroupType.CROSS
a.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
const logic = new TransactionLogic(a)
expect(logic.isBelongTogether(b)).toBe(false)
expect(spy).toHaveBeenLastCalledWith(
"cross group types don't match",
expect.objectContaining({}),
)
})
it('b is CROSS', () => {
body.type = CrossGroupType.CROSS
b.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
const logic = new TransactionLogic(a)
expect(logic.isBelongTogether(b)).toBe(false)
expect(spy).toHaveBeenLastCalledWith(
"cross group types don't match",
expect.objectContaining({}),
)
})
it('true with a as INBOUND and b as OUTBOUND', () => {
let body = TransactionBody.fromBodyBytes(a.bodyBytes)
body.type = CrossGroupType.INBOUND
a.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
body = TransactionBody.fromBodyBytes(b.bodyBytes)
body.type = CrossGroupType.OUTBOUND
b.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
const logic = new TransactionLogic(a)
expect(logic.isBelongTogether(b)).toBe(true)
})
})
describe('false because of transaction type not suitable for cross group transactions', () => {
const body = new TransactionBody()
body.type = CrossGroupType.OUTBOUND
it('without transaction type (broken TransactionBody)', () => {
a.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
const logic = new TransactionLogic(a)
expect(() => logic.isBelongTogether(b)).toThrowError("couldn't determine transaction type")
})
it('not the same transaction types', () => {
const body = new TransactionBody()
body.type = CrossGroupType.OUTBOUND
body.registerAddress = new RegisterAddress()
a.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
const logic = new TransactionLogic(a)
expect(logic.isBelongTogether(b)).toBe(false)
expect(spy).toHaveBeenLastCalledWith(
"transaction types don't match",
expect.objectContaining({}),
)
})
it('community root cannot be a cross group transaction', () => {
let body = new TransactionBody()
body.type = CrossGroupType.OUTBOUND
body.communityRoot = new CommunityRoot()
a.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
body = new TransactionBody()
body.type = CrossGroupType.INBOUND
body.communityRoot = new CommunityRoot()
b.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
const logic = new TransactionLogic(a)
expect(logic.isBelongTogether(b)).toBe(false)
expect(spy).toHaveBeenLastCalledWith(
"TransactionType COMMUNITY_ROOT couldn't be a CrossGroup Transaction",
)
})
it('Gradido Creation cannot be a cross group transaction', () => {
let body = new TransactionBody()
body.type = CrossGroupType.OUTBOUND
body.creation = new GradidoCreation()
a.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
body = new TransactionBody()
body.type = CrossGroupType.INBOUND
body.creation = new GradidoCreation()
b.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
const logic = new TransactionLogic(a)
expect(logic.isBelongTogether(b)).toBe(false)
expect(spy).toHaveBeenLastCalledWith(
"TransactionType GRADIDO_CREATION couldn't be a CrossGroup Transaction",
)
})
it('Deferred Transfer cannot be a cross group transaction', () => {
let body = new TransactionBody()
body.type = CrossGroupType.OUTBOUND
body.deferredTransfer = new GradidoDeferredTransfer()
a.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
body = new TransactionBody()
body.type = CrossGroupType.INBOUND
body.deferredTransfer = new GradidoDeferredTransfer()
b.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
const logic = new TransactionLogic(a)
expect(logic.isBelongTogether(b)).toBe(false)
expect(spy).toHaveBeenLastCalledWith(
"TransactionType GRADIDO_DEFERRED_TRANSFER couldn't be a CrossGroup Transaction",
)
})
})
describe('false because of wrong amount', () => {
it('amount missing on a', () => {
a.amount = undefined
const logic = new TransactionLogic(a)
expect(logic.isBelongTogether(b)).toBe(false)
expect(spy).toHaveBeenLastCalledWith('missing amount')
})
it('amount missing on b', () => {
b.amount = undefined
const logic = new TransactionLogic(a)
expect(logic.isBelongTogether(b)).toBe(false)
expect(spy).toHaveBeenLastCalledWith('missing amount')
})
it('amount not the same', () => {
a.amount = new Decimal('101')
const logic = new TransactionLogic(a)
expect(logic.isBelongTogether(b)).toBe(false)
expect(spy).toHaveBeenLastCalledWith('amounts mismatch', expect.objectContaining({}))
})
})
it('false because otherGroup are the same', () => {
const body = new TransactionBody()
body.type = CrossGroupType.OUTBOUND
body.transfer = new GradidoTransfer()
body.otherGroup = 'sending group'
a.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
const logic = new TransactionLogic(a)
expect(logic.isBelongTogether(b)).toBe(false)
expect(spy).toHaveBeenLastCalledWith('otherGroups are the same', expect.objectContaining({}))
})
it('false because of different memos', () => {
const body = new TransactionBody()
body.type = CrossGroupType.OUTBOUND
body.transfer = new GradidoTransfer()
body.otherGroup = 'recipient group'
body.memo = 'changed memo'
a.bodyBytes = Buffer.from(TransactionBody.encode(body).finish())
const logic = new TransactionLogic(a)
expect(logic.isBelongTogether(b)).toBe(false)
expect(spy).toHaveBeenLastCalledWith('memo differ', expect.objectContaining({}))
})
})
})

View File

@ -0,0 +1,200 @@
import { Transaction } from '@entity/Transaction'
import { Not } from 'typeorm'
import { logger } from '@/logging/logger'
import { TransactionBodyLoggingView } from '@/logging/TransactionBodyLogging.view'
import { TransactionLoggingView } from '@/logging/TransactionLogging.view'
import { LogError } from '@/server/LogError'
import { CrossGroupType } from './proto/3_3/enum/CrossGroupType'
import { TransactionType } from './proto/3_3/enum/TransactionType'
import { TransactionBody } from './proto/3_3/TransactionBody'
export class TransactionLogic {
protected transactionBody: TransactionBody | undefined
// eslint-disable-next-line no-useless-constructor
public constructor(private self: Transaction) {}
/**
* search for transaction pair for Cross Group Transaction
* @returns
*/
public async findPairTransaction(): Promise<Transaction> {
const type = this.getBody().type
if (type === CrossGroupType.LOCAL) {
throw new LogError("local transaction don't has a pairing transaction")
}
// check if already was loaded from db
if (this.self.pairingTransaction) {
return this.self.pairingTransaction
}
if (this.self.pairingTransaction) {
const pairingTransaction = await Transaction.findOneBy({ id: this.self.pairingTransaction })
if (pairingTransaction) {
return pairingTransaction
}
}
// check if we find some in db
const sameCreationDateTransactions = await Transaction.findBy({
createdAt: this.self.createdAt,
id: Not(this.self.id),
})
if (
sameCreationDateTransactions.length === 1 &&
this.isBelongTogether(sameCreationDateTransactions[0])
) {
return sameCreationDateTransactions[0]
}
// this approach only work if all entities get ids really incremented by one
if (type === CrossGroupType.OUTBOUND) {
const prevTransaction = await Transaction.findOneBy({ id: this.self.id - 1 })
if (prevTransaction && this.isBelongTogether(prevTransaction)) {
return prevTransaction
}
} else if (type === CrossGroupType.INBOUND) {
const nextTransaction = await Transaction.findOneBy({ id: this.self.id + 1 })
if (nextTransaction && this.isBelongTogether(nextTransaction)) {
return nextTransaction
}
}
throw new LogError("couldn't find valid pairing transaction", {
id: this.self.id,
type: CrossGroupType[type],
transactionCountWithSameCreatedAt: sameCreationDateTransactions.length,
})
}
/**
* check if two transactions belong together
* are they pairs for a cross group transaction
* @param otherTransaction
*/
public isBelongTogether(otherTransaction: Transaction): boolean {
if (this.self.id === otherTransaction.id) {
logger.info('id is the same, it is the same transaction!')
return false
}
if (
this.self.signingAccountId !== otherTransaction.signingAccountId ||
this.self.recipientAccountId !== otherTransaction.recipientAccountId ||
this.self.communityId !== otherTransaction.otherCommunityId ||
this.self.otherCommunityId !== otherTransaction.communityId ||
this.self.createdAt.getTime() !== otherTransaction.createdAt.getTime()
) {
logger.info('transaction a and b are not pairs', {
a: new TransactionLoggingView(this.self).toJSON(),
b: new TransactionLoggingView(otherTransaction).toJSON(),
})
return false
}
const body = this.getBody()
const otherBody = TransactionBody.fromBodyBytes(otherTransaction.bodyBytes)
/**
* both must be Cross or
* one can be OUTBOUND and one can be INBOUND
* no one can be LOCAL
*/
if (!this.validCrossGroupTypes(body.type, otherBody.type)) {
logger.info("cross group types don't match", {
a: new TransactionBodyLoggingView(body).toJSON(),
b: new TransactionBodyLoggingView(otherBody).toJSON(),
})
return false
}
const type = body.getTransactionType()
const otherType = otherBody.getTransactionType()
if (!type || !otherType) {
throw new LogError("couldn't determine transaction type", {
a: new TransactionBodyLoggingView(body).toJSON(),
b: new TransactionBodyLoggingView(otherBody).toJSON(),
})
}
if (type !== otherType) {
logger.info("transaction types don't match", {
a: new TransactionBodyLoggingView(body).toJSON(),
b: new TransactionBodyLoggingView(otherBody).toJSON(),
})
return false
}
if (
[
TransactionType.COMMUNITY_ROOT,
TransactionType.GRADIDO_CREATION,
TransactionType.GRADIDO_DEFERRED_TRANSFER,
].includes(type)
) {
logger.info(`TransactionType ${TransactionType[type]} couldn't be a CrossGroup Transaction`)
return false
}
if (
[
TransactionType.GRADIDO_CREATION,
TransactionType.GRADIDO_TRANSFER,
TransactionType.GRADIDO_DEFERRED_TRANSFER,
].includes(type)
) {
if (!this.self.amount || !otherTransaction.amount) {
logger.info('missing amount')
return false
}
if (this.self.amount.cmp(otherTransaction.amount.toString())) {
logger.info('amounts mismatch', {
a: this.self.amount.toString(),
b: otherTransaction.amount.toString(),
})
return false
}
}
if (body.otherGroup === otherBody.otherGroup) {
logger.info('otherGroups are the same', {
a: new TransactionBodyLoggingView(body).toJSON(),
b: new TransactionBodyLoggingView(otherBody).toJSON(),
})
return false
}
if (body.memo !== otherBody.memo) {
logger.info('memo differ', {
a: new TransactionBodyLoggingView(body).toJSON(),
b: new TransactionBodyLoggingView(otherBody).toJSON(),
})
return false
}
return true
}
/**
* both must be CROSS or
* one can be OUTBOUND and one can be INBOUND
* no one can be LOCAL
* @return true if crossGroupTypes are valid
*/
protected validCrossGroupTypes(a: CrossGroupType, b: CrossGroupType): boolean {
logger.debug('compare ', {
a: CrossGroupType[a],
b: CrossGroupType[b],
})
if (a === CrossGroupType.LOCAL || b === CrossGroupType.LOCAL) {
logger.info('no one can be LOCAL')
return false
}
if (
(a === CrossGroupType.INBOUND && b === CrossGroupType.OUTBOUND) ||
(a === CrossGroupType.OUTBOUND && b === CrossGroupType.INBOUND)
) {
return true // One can be INBOUND and one can be OUTBOUND
}
return a === CrossGroupType.CROSS && b === CrossGroupType.CROSS
}
public getBody(): TransactionBody {
if (!this.transactionBody) {
this.transactionBody = TransactionBody.fromBodyBytes(this.self.bodyBytes)
}
return this.transactionBody
}
}

View File

@ -12,7 +12,7 @@ export class UserLogic {
/**
*
* @param parentKeys if undefined use home community key pair
* @param parentKeys from home community for own user
* @returns
*/

View File

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

View File

@ -1,8 +1,5 @@
import { Field, Message } from 'protobufjs'
import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType'
import { TransactionError } from '@/graphql/model/TransactionError'
import { logger } from '@/logging/logger'
import { LogError } from '@/server/LogError'
import { SignatureMap } from './SignatureMap'
@ -46,14 +43,6 @@ export class GradidoTransaction extends Message<GradidoTransaction> {
}
getTransactionBody(): TransactionBody {
try {
return TransactionBody.decode(new Uint8Array(this.bodyBytes))
} catch (error) {
logger.error('error decoding body from gradido transaction: %s', error)
throw new TransactionError(
TransactionErrorType.PROTO_DECODE_ERROR,
'cannot decode body from gradido transaction',
)
}
return TransactionBody.fromBodyBytes(this.bodyBytes)
}
}

View File

@ -1,8 +1,11 @@
import { Transaction } from '@entity/Transaction'
import { Field, Message, OneOf } from 'protobufjs'
import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType'
import { CommunityDraft } from '@/graphql/input/CommunityDraft'
import { TransactionDraft } from '@/graphql/input/TransactionDraft'
import { TransactionError } from '@/graphql/model/TransactionError'
import { logger } from '@/logging/logger'
import { LogError } from '@/server/LogError'
import { timestampToDate } from '@/utils/typeConverter'
@ -36,6 +39,18 @@ export class TransactionBody extends Message<TransactionBody> {
}
}
public static fromBodyBytes(bodyBytes: Buffer) {
try {
return TransactionBody.decode(new Uint8Array(bodyBytes))
} catch (error) {
logger.error('error decoding body from gradido transaction: %s', error)
throw new TransactionError(
TransactionErrorType.PROTO_DECODE_ERROR,
'cannot decode body from gradido transaction',
)
}
}
@Field.d(1, 'string')
public memo: string

View File

@ -9,9 +9,9 @@ export class UserIdentifier {
@IsUUID('4')
uuid: string
@Field(() => String, { nullable: true })
@Field(() => String)
@IsUUID('4')
communityUuid?: string
communityUuid: string
@Field(() => Int, { defaultValue: 1, nullable: true })
@IsPositive()

View File

@ -2,11 +2,13 @@ import { Resolver, Arg, Mutation } from 'type-graphql'
import { TransactionDraft } from '@input/TransactionDraft'
import { TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY } from '@/data/const'
import { TransactionRepository } from '@/data/Transaction.repository'
import { CreateTransactionRecipeContext } from '@/interactions/backendToDb/transaction/CreateTransationRecipe.context'
import { BackendTransactionLoggingView } from '@/logging/BackendTransactionLogging.view'
import { logger } from '@/logging/logger'
import { TransactionLoggingView } from '@/logging/TransactionLogging.view'
import { InterruptiveSleepManager } from '@/manager/InterruptiveSleepManager'
import { LogError } from '@/server/LogError'
import { TransactionError } from '../model/TransactionError'
@ -48,6 +50,7 @@ export class TransactionResolver {
// we can store the transaction and with that automatic the backend transaction
await transactionRecipe.save()
}
InterruptiveSleepManager.getInstance().interrupt(TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY)
return new TransactionResult(new TransactionRecipe(transactionRecipe))
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {

View File

@ -1,17 +1,29 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { CONFIG } from '@/config'
import { Mnemonic } from './data/Mnemonic'
import createServer from './server/createServer'
import { stopTransmitToIota, transmitToIota } from './tasks/transmitToIota'
async function main() {
if (CONFIG.IOTA_HOME_COMMUNITY_SEED) {
Mnemonic.validateSeed(CONFIG.IOTA_HOME_COMMUNITY_SEED)
}
// eslint-disable-next-line no-console
console.log(`DLT_CONNECTOR_PORT=${CONFIG.DLT_CONNECTOR_PORT}`)
const { app } = await createServer()
// loop run all the time, check for new transaction for sending to iota
void transmitToIota()
app.listen(CONFIG.DLT_CONNECTOR_PORT, () => {
// eslint-disable-next-line no-console
console.log(`Server is running at http://localhost:${CONFIG.DLT_CONNECTOR_PORT}`)
})
process.on('exit', () => {
// Add shutdown logic here.
stopTransmitToIota()
})
}
main().catch((e) => {

View File

@ -0,0 +1,65 @@
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

@ -3,6 +3,7 @@ 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'
@ -10,6 +11,7 @@ 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 { getDataSource } from '@/typeorm/DataSource'
import { CreateTransactionRecipeContext } from '../transaction/CreateTransationRecipe.context'
@ -36,12 +38,14 @@ export class HomeCommunityRole extends CommunityRole {
public async store(): Promise<Community> {
try {
return await getDataSource().transaction(async (transactionalEntityManager) => {
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(

View File

@ -3,6 +3,7 @@ import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType'
import { TransactionDraft } from '@/graphql/input/TransactionDraft'
import { UserIdentifier } from '@/graphql/input/UserIdentifier'
import { TransactionError } from '@/graphql/model/TransactionError'
import { iotaTopicFromCommunityUUID } from '@/utils/typeConverter'
export abstract class AbstractTransactionRole {
// eslint-disable-next-line no-useless-constructor
@ -26,7 +27,7 @@ export abstract class AbstractTransactionRole {
* OUTBOUND: stored on 'gdd1', otherGroup: 'gdd2'
* INBOUND: goes to receiver, stored on receiver community blockchain
* INBOUND: stored on 'gdd2', otherGroup: 'gdd1'
* @returns
* @returns iota topic
*/
public getOtherGroup(): string {
let user: UserIdentifier
@ -42,7 +43,7 @@ export abstract class AbstractTransactionRole {
'missing sender/signing user community id for cross group transaction',
)
}
return user.communityUuid
return iotaTopicFromCommunityUUID(user.communityUuid)
case CrossGroupType.OUTBOUND:
user = this.getRecipientUser()
if (!user.communityUuid) {
@ -51,7 +52,7 @@ export abstract class AbstractTransactionRole {
'missing recipient user community id for cross group transaction',
)
}
return user.communityUuid
return iotaTopicFromCommunityUUID(user.communityUuid)
default:
throw new TransactionError(
TransactionErrorType.NOT_IMPLEMENTED_YET,

View File

@ -0,0 +1,340 @@
import 'reflect-metadata'
import { Account } from '@entity/Account'
import { Decimal } from 'decimal.js-light'
import { v4 } from 'uuid'
import { TestDB } from '@test/TestDB'
import { CONFIG } from '@/config'
import { KeyPair } from '@/data/KeyPair'
import { Mnemonic } from '@/data/Mnemonic'
import { CrossGroupType } from '@/data/proto/3_3/enum/CrossGroupType'
import { TransactionType } from '@/data/proto/3_3/enum/TransactionType'
import { TransactionBody } from '@/data/proto/3_3/TransactionBody'
import { InputTransactionType } from '@/graphql/enum/InputTransactionType'
import { TransactionDraft } from '@/graphql/input/TransactionDraft'
import { iotaTopicFromCommunityUUID } from '@/utils/typeConverter'
import { CreateTransactionRecipeContext } from './CreateTransationRecipe.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(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 topic = iotaTopicFromCommunityUUID(homeCommunityUuid)
const foreignTopic = iotaTopicFromCommunityUUID(foreignCommunityUuid)
describe('interactions/backendToDb/transaction/Create Transaction Recipe Context Test', () => {
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('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()
// console.log(new TransactionLoggingView(transaction))
expect(transaction).toMatchObject({
type: TransactionType.GRADIDO_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 body = TransactionBody.fromBodyBytes(transaction.bodyBytes)
// console.log(new TransactionBodyLoggingView(body))
expect(body.creation).toBeDefined()
if (!body.creation) throw new Error()
const bodyReceiverPubkey = Buffer.from(body.creation.recipient.pubkey)
expect(bodyReceiverPubkey.compare(firstUser.account.derive2Pubkey)).toBe(0)
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 body = TransactionBody.fromBodyBytes(transaction.bodyBytes)
// console.log(new TransactionBodyLoggingView(body))
expect(body.transfer).toBeDefined()
if (!body.transfer) throw new Error()
expect(Buffer.from(body.transfer.recipient).compare(secondUser.account.derive2Pubkey)).toBe(0)
expect(Buffer.from(body.transfer.sender.pubkey).compare(firstUser.account.derive2Pubkey)).toBe(
0,
)
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 body = TransactionBody.fromBodyBytes(transaction.bodyBytes)
// console.log(new TransactionBodyLoggingView(body))
expect(body.transfer).toBeDefined()
if (!body.transfer) throw new Error()
expect(Buffer.from(body.transfer.recipient).compare(secondUser.account.derive2Pubkey)).toBe(0)
expect(Buffer.from(body.transfer.sender.pubkey).compare(firstUser.account.derive2Pubkey)).toBe(
0,
)
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 body = TransactionBody.fromBodyBytes(transaction.bodyBytes)
// console.log(new TransactionBodyLoggingView(body))
expect(body.transfer).toBeDefined()
if (!body.transfer) throw new Error()
expect(Buffer.from(body.transfer.recipient).compare(foreignUser.account.derive2Pubkey)).toBe(0)
expect(Buffer.from(body.transfer.sender.pubkey).compare(firstUser.account.derive2Pubkey)).toBe(
0,
)
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 body = TransactionBody.fromBodyBytes(transaction.bodyBytes)
// console.log(new TransactionBodyLoggingView(body))
expect(body.transfer).toBeDefined()
if (!body.transfer) throw new Error()
expect(Buffer.from(body.transfer.recipient).compare(foreignUser.account.derive2Pubkey)).toBe(0)
expect(Buffer.from(body.transfer.sender.pubkey).compare(firstUser.account.derive2Pubkey)).toBe(
0,
)
expect(body).toMatchObject({
type: CrossGroupType.INBOUND,
otherGroup: topic,
transfer: {
sender: {
amount: '100',
},
},
})
})
})

View File

@ -1,5 +1,12 @@
import { Community } from '@entity/Community'
import { CommunityRepository } from '@/data/Community.repository'
import { CrossGroupType } from '@/data/proto/3_3/enum/CrossGroupType'
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'
@ -15,4 +22,26 @@ export class CreationTransactionRole extends AbstractTransactionRole {
public getCrossGroupType(): CrossGroupType {
return CrossGroupType.LOCAL
}
public async getCommunity(): Promise<Community> {
if (this.self.user.communityUuid !== this.self.linkedUser.communityUuid) {
throw new TransactionError(
TransactionErrorType.LOGIC_ERROR,
'mismatch community uuids on creation transaction',
)
}
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,6 +1,9 @@
import { Community } from '@entity/Community'
import { Transaction } from '@entity/Transaction'
import { AccountLogic } from '@/data/Account.logic'
import { KeyPair } from '@/data/KeyPair'
import { CrossGroupType } from '@/data/proto/3_3/enum/CrossGroupType'
import { TransactionBodyBuilder } from '@/data/proto/TransactionBody.builder'
import { TransactionBuilder } from '@/data/Transaction.builder'
import { UserRepository } from '@/data/User.repository'
@ -52,18 +55,34 @@ export class TransactionRecipeRole {
this.transactionBuilder
.fromTransactionBodyBuilder(transactionBodyBuilder)
.addBackendTransaction(transactionDraft)
await this.transactionBuilder.setSenderCommunityFromSenderUser(signingUser)
await this.transactionBuilder.setCommunityFromUser(transactionDraft.user)
if (recipientUser.communityUuid !== signingUser.communityUuid) {
await this.transactionBuilder.setOtherCommunityFromRecipientUser(recipientUser)
await this.transactionBuilder.setOtherCommunityFromUser(transactionDraft.linkedUser)
}
const transaction = this.transactionBuilder.getTransaction()
const communityKeyPair = new KeyPair(
this.getSigningCommunity(transactionTypeRole.getCrossGroupType()),
)
const accountLogic = new AccountLogic(signingAccount)
// sign
this.transactionBuilder.setSignature(
new KeyPair(this.transactionBuilder.getCommunity()).sign(transaction.bodyBytes),
accountLogic.calculateKeyPair(communityKeyPair).sign(transaction.bodyBytes),
)
return this
}
public getSigningCommunity(crossGroupType: CrossGroupType): Community {
if (crossGroupType === CrossGroupType.INBOUND) {
const otherCommunity = this.transactionBuilder.getOtherCommunity()
if (!otherCommunity) {
throw new TransactionError(TransactionErrorType.NOT_FOUND, 'missing other community')
}
return otherCommunity
}
return this.transactionBuilder.getCommunity()
}
public getTransaction(): Transaction {
return this.transactionBuilder.getTransaction()
}

View File

@ -0,0 +1,90 @@
import { Transaction } from '@entity/Transaction'
import { sendMessage as iotaSendMessage } from '@/client/IotaClient'
import { KeyPair } from '@/data/KeyPair'
import { GradidoTransaction } from '@/data/proto/3_3/GradidoTransaction'
import { SignaturePair } from '@/data/proto/3_3/SignaturePair'
import { TransactionBody } from '@/data/proto/3_3/TransactionBody'
import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType'
import { TransactionError } from '@/graphql/model/TransactionError'
import { GradidoTransactionLoggingView } from '@/logging/GradidoTransactionLogging.view'
import { logger } from '@/logging/logger'
export abstract class AbstractTransactionRecipeRole {
protected transactionBody: TransactionBody
public constructor(protected self: Transaction) {
this.transactionBody = TransactionBody.fromBodyBytes(this.self.bodyBytes)
}
public abstract transmitToIota(): Promise<Transaction>
protected getGradidoTransaction(): GradidoTransaction {
const transaction = new GradidoTransaction(this.transactionBody)
if (!this.self.signature) {
throw new TransactionError(
TransactionErrorType.MISSING_PARAMETER,
'missing signature in transaction recipe',
)
}
const signaturePair = new SignaturePair()
if (this.self.signature.length !== 64) {
throw new TransactionError(TransactionErrorType.INVALID_SIGNATURE, "signature isn't 64 bytes")
}
signaturePair.signature = this.self.signature
if (this.transactionBody.communityRoot) {
const publicKey = this.self.community.rootPubkey
if (!publicKey) {
throw new TransactionError(
TransactionErrorType.MISSING_PARAMETER,
'missing community public key for community root transaction',
)
}
signaturePair.pubKey = publicKey
} else if (this.self.signingAccount) {
const publicKey = this.self.signingAccount.derive2Pubkey
if (!publicKey) {
throw new TransactionError(
TransactionErrorType.MISSING_PARAMETER,
'missing signing account public key for transaction',
)
}
signaturePair.pubKey = publicKey
} else {
throw new TransactionError(
TransactionErrorType.NOT_FOUND,
"signingAccount not exist and it isn't a community root transaction",
)
}
if (signaturePair.validate()) {
transaction.sigMap.sigPair.push(signaturePair)
}
if (!KeyPair.verify(transaction.bodyBytes, signaturePair)) {
logger.debug('invalid signature', new GradidoTransactionLoggingView(transaction))
throw new TransactionError(TransactionErrorType.INVALID_SIGNATURE, 'signature is invalid')
}
return transaction
}
/**
*
* @param gradidoTransaction
* @param topic
* @return iota message id
*/
protected async sendViaIota(
gradidoTransaction: GradidoTransaction,
topic: string,
): Promise<Buffer> {
// protobuf serializing function
const messageBuffer = GradidoTransaction.encode(gradidoTransaction).finish()
const resultMessage = await iotaSendMessage(
messageBuffer,
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

@ -0,0 +1,40 @@
import { Transaction } from '@entity/Transaction'
import { TransactionLogic } from '@/data/Transaction.logic'
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 async transmitToIota(): Promise<Transaction> {
logger.debug('transmit INBOUND transaction to iota', new TransactionLoggingView(this.self))
const gradidoTransaction = this.getGradidoTransaction()
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),
)
}
gradidoTransaction.parentMessageId = 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(
gradidoTransaction,
this.self.otherCommunity.iotaTopic,
)
return this.self
}
}

View File

@ -0,0 +1,25 @@
import { Transaction } from '@entity/Transaction'
import { CrossGroupType } from '@/data/proto/3_3/enum/CrossGroupType'
import { logger } from '@/logging/logger'
import { TransactionLoggingView } from '@/logging/TransactionLogging.view'
import { AbstractTransactionRecipeRole } from './AbstractTransactionRecipe.role'
export class LocalTransactionRecipeRole extends AbstractTransactionRecipeRole {
public async transmitToIota(): Promise<Transaction> {
let transactionCrossGroupTypeName = 'LOCAL'
if (this.transactionBody) {
transactionCrossGroupTypeName = CrossGroupType[this.transactionBody.type]
}
logger.debug(
`transmit ${transactionCrossGroupTypeName} transaction to iota`,
new TransactionLoggingView(this.self),
)
this.self.iotaMessageId = await this.sendViaIota(
this.getGradidoTransaction(),
this.self.community.iotaTopic,
)
return this.self
}
}

View File

@ -0,0 +1,6 @@
import { LocalTransactionRecipeRole } from './LocalTransactionRecipe.role'
/**
* Outbound Transaction on sender community, mark the gradidos as sended out of community
*/
export class OutboundTransactionRecipeRole extends LocalTransactionRecipeRole {}

View File

@ -0,0 +1,168 @@
import 'reflect-metadata'
import { Account } from '@entity/Account'
import { Decimal } from 'decimal.js-light'
import { v4 } from 'uuid'
import { TestDB } from '@test/TestDB'
import { CONFIG } from '@/config'
import { KeyPair } from '@/data/KeyPair'
import { Mnemonic } from '@/data/Mnemonic'
import { CrossGroupType } from '@/data/proto/3_3/enum/CrossGroupType'
import { TransactionBody } from '@/data/proto/3_3/TransactionBody'
import { InputTransactionType } from '@/graphql/enum/InputTransactionType'
import { TransactionDraft } from '@/graphql/input/TransactionDraft'
import { logger } from '@/logging/logger'
import { CreateTransactionRecipeContext } from '../backendToDb/transaction/CreateTransationRecipe.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('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 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 body = TransactionBody.fromBodyBytes(transaction.bodyBytes)
expect(body.type).toBe(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 body = TransactionBody.fromBodyBytes(transaction.bodyBytes)
expect(body.type).toBe(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

@ -0,0 +1,57 @@
import { Transaction } from '@entity/Transaction'
import { CrossGroupType } from '@/data/proto/3_3/enum/CrossGroupType'
import { TransactionBody } from '@/data/proto/3_3/TransactionBody'
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 transactionBody = TransactionBody.fromBodyBytes(transaction.bodyBytes)
switch (transactionBody.type) {
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.type)
}
}
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

@ -16,7 +16,7 @@ export class AccountLoggingView extends AbstractLoggingView {
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),
derive2Pubkey: this.account.derive2Pubkey.toString(this.bufferStringFormat),
type: getEnumValue(AddressType, this.account.type),
createdAt: this.dateToString(this.account.createdAt),
confirmedAt: this.dateToString(this.account.confirmedAt),

View File

@ -18,7 +18,7 @@ export class TransactionLoggingView extends AbstractLoggingView {
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public toJSON(showBackendTransactions = true): any {
public toJSON(showBackendTransactions = true, deep = 1): any {
return {
id: this.self.id,
nr: this.self.nr,
@ -31,16 +31,23 @@ export class TransactionLoggingView extends AbstractLoggingView {
community: new CommunityLoggingView(this.self.community).toJSON(),
otherCommunity: this.self.otherCommunity
? new CommunityLoggingView(this.self.otherCommunity)
: undefined,
: { id: this.self.otherCommunityId },
iotaMessageId: this.self.iotaMessageId
? this.self.iotaMessageId.toString(this.bufferStringFormat)
: undefined,
signingAccount: this.self.signingAccount
? new AccountLoggingView(this.self.signingAccount)
: undefined,
: { id: this.self.signingAccountId },
recipientAccount: this.self.recipientAccount
? new AccountLoggingView(this.self.recipientAccount)
: undefined,
: { 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),

View File

@ -10,7 +10,7 @@ export class TransferAmountLoggingView extends AbstractLoggingView {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public toJSON(): any {
return {
publicKey: Buffer.from(this.self.pubkey).toString(this.bufferStringFormat),
pubkey: Buffer.from(this.self.pubkey).toString(this.bufferStringFormat),
amount: this.self.amount,
communityId: this.self.communityId,
}

View File

@ -0,0 +1,63 @@
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

@ -0,0 +1,49 @@
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

@ -0,0 +1,31 @@
/**
* 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
}
}
}

View File

@ -1,13 +1,45 @@
import 'reflect-metadata'
import { Timestamp } from '@/data/proto/3_3/Timestamp'
import { timestampToDate } from './typeConverter'
import {
base64ToBuffer,
iotaTopicFromCommunityUUID,
timestampSecondsToDate,
timestampToDate,
uuid4ToBuffer,
} from './typeConverter'
describe('utils/typeConverter', () => {
it('uuid4ToBuffer', () => {
expect(uuid4ToBuffer('4f28e081-5c39-4dde-b6a4-3bde71de8d65')).toStrictEqual(
Buffer.from('4f28e0815c394ddeb6a43bde71de8d65', 'hex'),
)
})
it('iotaTopicFromCommunityUUID', () => {
expect(iotaTopicFromCommunityUUID('4f28e081-5c39-4dde-b6a4-3bde71de8d65')).toBe(
'3138b3590311fdf0a823e173caa9487b7d275c23fab07106b4b1364cb038affd',
)
})
it('timestampToDate', () => {
const now = new Date('Thu, 05 Oct 2023 11:55:18 +0000')
const now = new Date('Thu, 05 Oct 2023 11:55:18.102 +0000')
const timestamp = new Timestamp(now)
expect(timestamp.seconds).toBe(Math.round(now.getTime() / 1000))
expect(timestampToDate(timestamp)).toEqual(now)
})
it('timestampSecondsToDate', () => {
const now = new Date('Thu, 05 Oct 2023 11:55:18.102 +0000')
const timestamp = new Timestamp(now)
expect(timestamp.seconds).toBe(Math.round(now.getTime() / 1000))
expect(timestampSecondsToDate(timestamp)).toEqual(new Date('Thu, 05 Oct 2023 11:55:18 +0000'))
})
it('base64ToBuffer', () => {
expect(base64ToBuffer('MTizWQMR/fCoI+FzyqlIe30nXCP6sHEGtLE2TLA4r/0=')).toStrictEqual(
Buffer.from('3138b3590311fdf0a823e173caa9487b7d275c23fab07106b4b1364cb038affd', 'hex'),
)
})
})

View File

@ -0,0 +1,28 @@
import { Community } from '@entity/Community'
import { KeyPair } from '@/data/KeyPair'
import { CommunityDraft } from '@/graphql/input/CommunityDraft'
import { AddCommunityContext } from '@/interactions/backendToDb/community/AddCommunity.context'
import { iotaTopicFromCommunityUUID } from '@/utils/typeConverter'
export const communitySeed = async (
uuid: string,
foreign: boolean,
keyPair: KeyPair | undefined = undefined,
): Promise<Community> => {
const homeCommunityDraft = new CommunityDraft()
homeCommunityDraft.uuid = uuid
homeCommunityDraft.foreign = foreign
homeCommunityDraft.createdAt = new Date().toISOString()
const iotaTopic = iotaTopicFromCommunityUUID(uuid)
const addCommunityContext = new AddCommunityContext(homeCommunityDraft, iotaTopic)
await addCommunityContext.run()
const community = await Community.findOneOrFail({ where: { iotaTopic } })
if (foreign && keyPair) {
// that isn't entirely correct, normally only the public key from foreign community is know, and will be come form blockchain
keyPair.fillInCommunityKeys(community)
await community.save()
}
return community
}

View File

@ -0,0 +1,55 @@
import { Account } from '@entity/Account'
import { User } from '@entity/User'
import { v4 } from 'uuid'
import { AccountFactory } from '@/data/Account.factory'
import { KeyPair } from '@/data/KeyPair'
import { UserFactory } from '@/data/User.factory'
import { UserLogic } from '@/data/User.logic'
import { AccountType } from '@/graphql/enum/AccountType'
import { UserAccountDraft } from '@/graphql/input/UserAccountDraft'
import { UserIdentifier } from '@/graphql/input/UserIdentifier'
export type UserSet = {
identifier: UserIdentifier
user: User
account: Account
}
export const createUserIdentifier = (userUuid: string, communityUuid: string): UserIdentifier => {
const user = new UserIdentifier()
user.uuid = userUuid
user.communityUuid = communityUuid
return user
}
export const createUserAndAccount = (
userIdentifier: UserIdentifier,
communityKeyPair: KeyPair,
): Account => {
const accountDraft = new UserAccountDraft()
accountDraft.user = userIdentifier
accountDraft.createdAt = new Date().toISOString()
accountDraft.accountType = AccountType.COMMUNITY_HUMAN
const user = UserFactory.create(accountDraft, communityKeyPair)
const userLogic = new UserLogic(user)
const account = AccountFactory.createAccountFromUserAccountDraft(
accountDraft,
userLogic.calculateKeyPair(communityKeyPair),
)
account.user = user
return account
}
export const createUserSet = (communityUuid: string, communityKeyPair: KeyPair): UserSet => {
const identifier = createUserIdentifier(v4(), communityUuid)
const account = createUserAndAccount(identifier, communityKeyPair)
if (!account.user) {
throw Error('user missing')
}
return {
identifier,
account,
user: account.user,
}
}

View File

@ -63,7 +63,7 @@
"@entity/*": ["../dlt-database/entity/*", "../../dlt-database/build/entity/*"]
},
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
"typeRoots": ["node_modules/@types"], /* List of folders to include type definitions from. */
"typeRoots": ["node_modules/@types", "@types"], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */

View File

@ -6388,7 +6388,7 @@ uuid@^8.3.2:
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
uuid@^9.0.0:
uuid@^9.0.0, uuid@^9.0.1:
version "9.0.1"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30"
integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==

View File

@ -23,7 +23,7 @@ export class Transaction extends BaseEntity {
@Column({ name: 'iota_message_id', type: 'binary', length: 32, nullable: true })
iotaMessageId?: Buffer
@OneToOne(() => Transaction)
@OneToOne(() => Transaction, { cascade: ['update'] })
// eslint-disable-next-line no-use-before-define
paringTransaction?: Transaction

View File

@ -0,0 +1,128 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
ManyToOne,
OneToOne,
JoinColumn,
BaseEntity,
OneToMany,
} from 'typeorm'
import { Decimal } from 'decimal.js-light'
import { DecimalTransformer } from '../../src/typeorm/DecimalTransformer'
import { Account } from '../Account'
import { Community } from '../Community'
import { BackendTransaction } from '../BackendTransaction'
@Entity('transactions')
export class Transaction extends BaseEntity {
@PrimaryGeneratedColumn('increment', { unsigned: true, type: 'bigint' })
id: number
@Column({ name: 'iota_message_id', type: 'binary', length: 32, nullable: true })
iotaMessageId?: Buffer
@OneToOne(() => Transaction, { cascade: ['update'] })
// eslint-disable-next-line no-use-before-define
pairingTransaction?: Transaction
@Column({ name: 'pairing_transaction_id', type: 'bigint', unsigned: true, nullable: true })
pairingTransactionId?: number
// if transaction has a sender than it is also the sender account
@ManyToOne(() => Account, (account) => account.transactionSigning)
@JoinColumn({ name: 'signing_account_id' })
signingAccount?: Account
@Column({ name: 'signing_account_id', type: 'int', unsigned: true, nullable: true })
signingAccountId?: number
@ManyToOne(() => Account, (account) => account.transactionRecipient)
@JoinColumn({ name: 'recipient_account_id' })
recipientAccount?: Account
@Column({ name: 'recipient_account_id', type: 'int', unsigned: true, nullable: true })
recipientAccountId?: number
@ManyToOne(() => Community, (community) => community.transactions, {
eager: true,
})
@JoinColumn({ name: 'community_id' })
community: Community
@Column({ name: 'community_id', type: 'int', unsigned: true })
communityId: number
@ManyToOne(() => Community, (community) => community.friendCommunitiesTransactions)
@JoinColumn({ name: 'other_community_id' })
otherCommunity?: Community
@Column({ name: 'other_community_id', type: 'int', unsigned: true, nullable: true })
otherCommunityId?: number
@Column({
type: 'decimal',
precision: 40,
scale: 20,
nullable: true,
transformer: DecimalTransformer,
})
amount?: Decimal
// account balance for sender based on creation date
@Column({
name: 'account_balance_on_creation',
type: 'decimal',
precision: 40,
scale: 20,
nullable: true,
transformer: DecimalTransformer,
})
accountBalanceOnCreation?: Decimal
@Column({ type: 'tinyint' })
type: number
@Column({ name: 'created_at', type: 'datetime', precision: 3 })
createdAt: Date
@Column({ name: 'body_bytes', type: 'blob' })
bodyBytes: Buffer
@Column({ type: 'binary', length: 64, unique: true })
signature: Buffer
@Column({ name: 'protocol_version', type: 'varchar', length: 255, default: '1' })
protocolVersion: string
@Column({ type: 'bigint', nullable: true })
nr?: number
@Column({ name: 'running_hash', type: 'binary', length: 48, nullable: true })
runningHash?: Buffer
// account balance for sender based on confirmation date (iota milestone)
@Column({
name: 'account_balance_on_confirmation',
type: 'decimal',
precision: 40,
scale: 20,
nullable: true,
transformer: DecimalTransformer,
})
accountBalanceOnConfirmation?: Decimal
@Column({ name: 'iota_milestone', type: 'bigint', nullable: true })
iotaMilestone?: number
// use timestamp from iota milestone which is only in seconds precision, so no need to use 3 Bytes extra here
@Column({ name: 'confirmed_at', type: 'datetime', nullable: true })
confirmedAt?: Date
@OneToMany(() => BackendTransaction, (backendTransaction) => backendTransaction.transaction, {
cascade: ['insert', 'update'],
})
@JoinColumn({ name: 'transaction_id' })
backendTransactions: BackendTransaction[]
}

View File

@ -1 +1 @@
export { Transaction } from './0003-refactor_transaction_recipe/Transaction'
export { Transaction } from './0004-fix_spelling/Transaction'

View File

@ -0,0 +1,15 @@
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
await queryFn(`
ALTER TABLE \`transactions\`
RENAME COLUMN \`paring_transaction_id\` TO \`pairing_transaction_id\`,
;
`)
}
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
await queryFn(`
ALTER TABLE \`transactions\`
RENAME COLUMN \`pairing_transaction_id\` TO \`paring_transaction_id\`,
;
`)
}