create context and roles for transmit to iota

This commit is contained in:
einhorn_b 2024-01-23 18:40:34 +01:00
parent f5f7e5fb63
commit 35be61a9c8
14 changed files with 389 additions and 61 deletions

View File

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

View File

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

View File

@ -0,0 +1,161 @@
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 on entity
if (this.self.paringTransaction) {
return this.self.paringTransaction
}
if (this.self.paringTransactionId) {
const pairingTransaction = await Transaction.findOneBy({ id: this.self.paringTransactionId })
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 paring 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.communityId ||
this.self.otherCommunityId !== otherTransaction.otherCommunityId ||
this.self.amount !== otherTransaction.amount ||
this.self.accountBalanceOnCreation !== otherTransaction.accountBalanceOnCreation ||
this.self.createdAt !== otherTransaction.createdAt
) {
logger.debug('transaction a and b are not pairs', {
a: new TransactionLoggingView(this.self),
b: new TransactionLoggingView(otherTransaction),
})
return false
}
const body = this.getBody()
const otherBody = TransactionBody.fromBodyBytes(otherTransaction.bodyBytes)
/**
* both can be Cross
* one can be OUTBOUND and one can be INBOUND
* no one can be LOCAL
*/
if (
(body.type === otherBody.type && body.type !== CrossGroupType.CROSS) ||
body.type === CrossGroupType.LOCAL ||
otherBody.type === CrossGroupType.LOCAL
) {
logger.info("cross group types don't match", {
a: new TransactionBodyLoggingView(body),
b: new TransactionBodyLoggingView(otherBody),
})
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),
b: new TransactionBodyLoggingView(otherBody),
})
}
if (type !== otherType) {
logger.info("transaction types don't match", {
a: new TransactionBodyLoggingView(body),
b: new TransactionBodyLoggingView(otherBody),
})
return false
}
if (
[
TransactionType.COMMUNITY_ROOT,
TransactionType.GRADIDO_CREATION,
TransactionType.GRADIDO_DEFERRED_TRANSFER,
].includes(type)
) {
logger.info(`TransactionType ${type} couldn't be a CrossGroup Transaction`)
return false
}
if (body.otherGroup === otherBody.otherGroup) {
logger.info('otherGroups are the same', {
a: new TransactionBodyLoggingView(body),
b: new TransactionBodyLoggingView(otherBody),
})
return false
}
if (body.memo !== otherBody.memo) {
logger.info('memo differ', {
a: new TransactionBodyLoggingView(body),
b: new TransactionBodyLoggingView(otherBody),
})
return false
}
return true
}
public getBody(): TransactionBody {
if (!this.transactionBody) {
this.transactionBody = TransactionBody.fromBodyBytes(this.self.bodyBytes)
}
return this.transactionBody
}
}

View File

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

View File

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

View File

@ -1,6 +1,7 @@
import { TransactionDraft } from '@input/TransactionDraft'
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'

View File

@ -1,41 +0,0 @@
import { Transaction } from '@entity/Transaction'
import { logger } from '@/logging/logger'
import { TransactionLoggingView } from '@/logging/TransactionLogging.view'
/**
* @DCI-Context
* Context for sending transaction recipe to iota
* send every transaction only once to iota!
*/
export class TransmitToIotaContext {
// eslint-disable-next-line no-useless-constructor
public constructor(private transaction: Transaction) {
}
public async run(): Promise<void> {
logger.info('transmit to iota', new TransactionLoggingView(this.transaction))
const recipeController = new TransactionRecipe(recipe)
const { transaction, body } = recipeController.getGradidoTransaction()
const messageBuffer = GradidoTransaction.encode(transaction).finish()
if (body.type === CrossGroupType.LOCAL) {
const resultMessage = await iotaSendMessage(
messageBuffer,
Buffer.from(recipe.community.iotaTopic, 'hex'),
)
recipe.iotaMessageId = Buffer.from(resultMessage.messageId, 'hex')
logger.info('transmitted Gradido Transaction to Iota', {
id: recipe.id,
messageId: resultMessage.messageId,
})
await getDataSource().manager.save(recipe)
} else {
throw new TransactionError(
TransactionErrorType.NOT_IMPLEMENTED_YET,
'other as crossGroupType Local not implemented yet',
)
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,42 @@
import { Transaction } from '@entity/Transaction'
import { CrossGroupType } from '@/data/proto/3_3/enum/CrossGroupType'
import { TransactionBody } from '@/data/proto/3_3/TransactionBody'
import { LogError } from '@/server/LogError'
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()
// store changes in db
await transaction.save()
}
}

View File

@ -1,6 +1,6 @@
import { TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY } from '@/data/const'
import { TransactionRepository } from '@/data/Transaction.repository'
import { TransmitToIotaContext } from '@/interactions/backendToDb/transmitToIota/TransmitToIota.context'
import { TransmitToIotaContext } from '@/interactions/transmitToIota/TransmitToIota.context'
import { InterruptiveSleepManager } from '@/manager/InterruptiveSleepManager'
import { logger } from '../logging/logger'
@ -36,7 +36,6 @@ export const transmitToIota = async (): Promise<void> => {
await InterruptiveSleepManager.getInstance().sleep(
TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY,
// 1000,
1000,
)
} catch (error) {
@ -44,7 +43,7 @@ export const transmitToIota = async (): Promise<void> => {
await sleep(10000)
}
}
logger.info(
logger.error(
'end iota message transmitter, no further transaction will be transmitted. !!! Please restart Server !!!',
)
}

View File

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