mirror of
https://github.com/IT4Change/gradido.git
synced 2026-02-06 09:56:05 +00:00
Merge branch 'master' into admin_update_community_list
This commit is contained in:
commit
eba52319be
@ -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'],
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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')
|
||||
})
|
||||
|
||||
|
||||
@ -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()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -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 = {
|
||||
|
||||
35
dlt-connector/src/data/Account.logic.ts
Normal file
35
dlt-connector/src/data/Account.logic.ts
Normal 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
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
|
||||
323
dlt-connector/src/data/Transaction.logic.test.ts
Normal file
323
dlt-connector/src/data/Transaction.logic.test.ts
Normal 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({}))
|
||||
})
|
||||
})
|
||||
})
|
||||
200
dlt-connector/src/data/Transaction.logic.ts
Normal file
200
dlt-connector/src/data/Transaction.logic.ts
Normal 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
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
*/
|
||||
|
||||
|
||||
1
dlt-connector/src/data/const.ts
Normal file
1
dlt-connector/src/data/const.ts
Normal file
@ -0,0 +1 @@
|
||||
export const TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY = 'transmitToIota'
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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'),
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -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(
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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',
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
|
||||
@ -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')
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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 {}
|
||||
@ -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({}),
|
||||
)
|
||||
})
|
||||
})
|
||||
@ -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')
|
||||
}
|
||||
}
|
||||
@ -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),
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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,
|
||||
}
|
||||
|
||||
63
dlt-connector/src/manager/InterruptiveSleepManager.ts
Normal file
63
dlt-connector/src/manager/InterruptiveSleepManager.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
49
dlt-connector/src/tasks/transmitToIota.ts
Normal file
49
dlt-connector/src/tasks/transmitToIota.ts
Normal 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 !!!',
|
||||
)
|
||||
}
|
||||
31
dlt-connector/src/utils/InterruptiveSleep.ts
Normal file
31
dlt-connector/src/utils/InterruptiveSleep.ts
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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'),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
28
dlt-connector/test/seeding/Community.seed.ts
Normal file
28
dlt-connector/test/seeding/Community.seed.ts
Normal 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
|
||||
}
|
||||
55
dlt-connector/test/seeding/UserSet.seed.ts
Normal file
55
dlt-connector/test/seeding/UserSet.seed.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
@ -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'. */
|
||||
|
||||
@ -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==
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
128
dlt-database/entity/0004-fix_spelling/Transaction.ts
Normal file
128
dlt-database/entity/0004-fix_spelling/Transaction.ts
Normal 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[]
|
||||
}
|
||||
@ -1 +1 @@
|
||||
export { Transaction } from './0003-refactor_transaction_recipe/Transaction'
|
||||
export { Transaction } from './0004-fix_spelling/Transaction'
|
||||
|
||||
15
dlt-database/migrations/0004-fix_spelling.ts
Normal file
15
dlt-database/migrations/0004-fix_spelling.ts
Normal 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\`,
|
||||
;
|
||||
`)
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user