mirror of
https://github.com/IT4Change/gradido.git
synced 2026-02-06 09:56:05 +00:00
move code into interaction in backend, implement transaction link in dlt-connector
This commit is contained in:
parent
4875621699
commit
e691db05b7
@ -14,6 +14,7 @@ import { LogError } from '@/server/LogError'
|
||||
import { backendLogger as logger } from '@/server/logger'
|
||||
|
||||
import { DltConnectorClient } from './DltConnectorClient'
|
||||
import { TransactionDraft } from './model/TransactionDraft'
|
||||
|
||||
let con: Connection
|
||||
|
||||
@ -113,7 +114,9 @@ describe('transmitTransaction', () => {
|
||||
const localTransaction = new DbTransaction()
|
||||
localTransaction.typeId = 12
|
||||
try {
|
||||
await DltConnectorClient.getInstance()?.transmitTransaction(localTransaction)
|
||||
await DltConnectorClient.getInstance()?.transmitTransaction(
|
||||
new TransactionDraft(localTransaction),
|
||||
)
|
||||
} catch (e) {
|
||||
expect(e).toMatchObject(
|
||||
new LogError('invalid transaction type id: ' + localTransaction.typeId.toString()),
|
||||
|
||||
@ -1,6 +1,3 @@
|
||||
import { Transaction as DbTransaction } from '@entity/Transaction'
|
||||
import { TransactionLink } from '@entity/TransactionLink'
|
||||
import { User } from '@entity/User'
|
||||
import { gql, GraphQLClient } from 'graphql-request'
|
||||
// eslint-disable-next-line import/named, n/no-extraneous-import
|
||||
import { FetchError } from 'node-fetch'
|
||||
@ -9,6 +6,7 @@ import { CONFIG } from '@/config'
|
||||
import { TransactionTypeId } from '@/graphql/enum/TransactionTypeId'
|
||||
import { LogError } from '@/server/LogError'
|
||||
import { backendLogger as logger } from '@/server/logger'
|
||||
// eslint-disable-next-line import/named, n/no-extraneous-import
|
||||
|
||||
import { TransactionDraft } from './model/TransactionDraft'
|
||||
import { TransactionResult } from './model/TransactionResult'
|
||||
@ -100,17 +98,6 @@ export class DltConnectorClient {
|
||||
return DltConnectorClient.instance
|
||||
}
|
||||
|
||||
private getTransactionParams(
|
||||
input: DbTransaction | User | TransactionLink,
|
||||
): TransactionDraft | UserAccountDraft {
|
||||
if (input instanceof DbTransaction || input instanceof TransactionLink) {
|
||||
return new TransactionDraft(input)
|
||||
} else if (input instanceof User) {
|
||||
return new UserAccountDraft(input)
|
||||
}
|
||||
throw new LogError('transaction should be either Transaction or User Entity')
|
||||
}
|
||||
|
||||
private handleTransactionResult(result: TransactionResult) {
|
||||
if (result.error) {
|
||||
throw new Error(result.error.message)
|
||||
@ -141,17 +128,16 @@ export class DltConnectorClient {
|
||||
* and update dltTransactionId of transaction in db with iota message id
|
||||
*/
|
||||
public async transmitTransaction(
|
||||
transaction: DbTransaction | User | TransactionLink,
|
||||
input: TransactionDraft | UserAccountDraft,
|
||||
): Promise<TransactionResult | undefined> {
|
||||
// we don't need the receive transactions, there contain basically the same data as the send transactions
|
||||
if (
|
||||
transaction instanceof DbTransaction &&
|
||||
(transaction.typeId as TransactionTypeId) === TransactionTypeId.RECEIVE
|
||||
input instanceof TransactionDraft &&
|
||||
TransactionTypeId[input.type as keyof typeof TransactionTypeId] === TransactionTypeId.RECEIVE
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const input = this.getTransactionParams(transaction)
|
||||
try {
|
||||
logger.debug('transmit transaction or user to dlt connector', input)
|
||||
if (input instanceof TransactionDraft) {
|
||||
|
||||
@ -0,0 +1,63 @@
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import { ObjectLiteral, OrderByCondition, SelectQueryBuilder } from '@dbTools/typeorm'
|
||||
import { DltTransaction } from '@entity/DltTransaction'
|
||||
|
||||
import { TransactionDraft } from '@dltConnector/model/TransactionDraft'
|
||||
import { UserAccountDraft } from '@dltConnector/model/UserAccountDraft'
|
||||
|
||||
import { backendLogger as logger } from '@/server/logger'
|
||||
|
||||
export abstract class AbstractTransactionToDltRole<T extends ObjectLiteral> {
|
||||
protected self: T | null
|
||||
|
||||
// public interface
|
||||
public abstract initWithLast(): Promise<this>
|
||||
public abstract getTimestamp(): number
|
||||
public abstract convertToGraphqlInput(): TransactionDraft | UserAccountDraft
|
||||
public getEntity(): T | null {
|
||||
return this.self
|
||||
}
|
||||
|
||||
public async saveTransactionResult(messageId: string, error: string | null): Promise<void> {
|
||||
const dltTransaction = DltTransaction.create()
|
||||
dltTransaction.messageId = messageId
|
||||
dltTransaction.error = error
|
||||
this.setJoinId(dltTransaction)
|
||||
await DltTransaction.save(dltTransaction)
|
||||
if (dltTransaction.error) {
|
||||
logger.error(
|
||||
`Store dltTransaction with error: id=${dltTransaction.id}, error=${dltTransaction.error}`,
|
||||
)
|
||||
} else {
|
||||
logger.info(
|
||||
`Store dltTransaction: messageId=${dltTransaction.messageId}, id=${dltTransaction.id}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// intern
|
||||
protected abstract setJoinId(dltTransaction: DltTransaction): void
|
||||
|
||||
// helper
|
||||
protected createQueryForPendingItems(
|
||||
qb: SelectQueryBuilder<T>,
|
||||
joinCondition: string,
|
||||
orderBy: OrderByCondition,
|
||||
): Promise<T | null> {
|
||||
return qb
|
||||
.leftJoin(DltTransaction, 'dltTransaction', joinCondition)
|
||||
.where('dltTransaction.user_id IS NULL')
|
||||
.andWhere('dltTransaction.transaction_id IS NULL')
|
||||
.andWhere('dltTransaction.transaction_link_Id IS NULL')
|
||||
.orderBy(orderBy)
|
||||
.limit(1)
|
||||
.getOne()
|
||||
}
|
||||
|
||||
protected createDltTransactionEntry(messageId: string, error: string | null): DltTransaction {
|
||||
const dltTransaction = DltTransaction.create()
|
||||
dltTransaction.messageId = messageId
|
||||
dltTransaction.error = error
|
||||
return dltTransaction
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,45 @@
|
||||
import { DltTransaction } from '@entity/DltTransaction'
|
||||
import { TransactionLink } from '@entity/TransactionLink'
|
||||
|
||||
import { TransactionDraft } from '@dltConnector/model/TransactionDraft'
|
||||
import { UserAccountDraft } from '@dltConnector/model/UserAccountDraft'
|
||||
|
||||
import { LogError } from '@/server/LogError'
|
||||
|
||||
import { AbstractTransactionToDltRole } from './AbstractTransactionToDlt.role'
|
||||
|
||||
/**
|
||||
* send transactionLink as Deferred Transfers
|
||||
*/
|
||||
export class TransactionLinkToDltRole extends AbstractTransactionToDltRole<TransactionLink> {
|
||||
async initWithLast(): Promise<this> {
|
||||
this.self = await this.createQueryForPendingItems(
|
||||
TransactionLink.createQueryBuilder().leftJoinAndSelect('transactionLink.user', 'user'),
|
||||
'TransactionLink.id = dltTransaction.transactionLinkId',
|
||||
// eslint-disable-next-line camelcase
|
||||
{ TransactionLinkId_created_at: 'ASC', User_id: 'ASC' },
|
||||
)
|
||||
return this
|
||||
}
|
||||
|
||||
public getTimestamp(): number {
|
||||
if (!this.self) {
|
||||
return Infinity
|
||||
}
|
||||
return this.self.createdAt.getTime()
|
||||
}
|
||||
|
||||
public convertToGraphqlInput(): TransactionDraft | UserAccountDraft {
|
||||
if (!this.self) {
|
||||
throw new LogError('try to create dlt entry for empty transaction link')
|
||||
}
|
||||
return new TransactionDraft(this.self)
|
||||
}
|
||||
|
||||
protected setJoinId(dltTransaction: DltTransaction): void {
|
||||
if (!this.self) {
|
||||
throw new LogError('try to create dlt entry for empty transaction link')
|
||||
}
|
||||
dltTransaction.transactionLinkId = this.self.id
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,45 @@
|
||||
import { DltTransaction } from '@entity/DltTransaction'
|
||||
import { Transaction } from '@entity/Transaction'
|
||||
|
||||
import { TransactionDraft } from '@dltConnector/model/TransactionDraft'
|
||||
import { UserAccountDraft } from '@dltConnector/model/UserAccountDraft'
|
||||
|
||||
import { LogError } from '@/server/LogError'
|
||||
|
||||
import { AbstractTransactionToDltRole } from './AbstractTransactionToDlt.role'
|
||||
|
||||
/**
|
||||
* send transfer and creations transactions to dlt connector as GradidoTransfer and GradidoCreation
|
||||
*/
|
||||
export class TransactionToDltRole extends AbstractTransactionToDltRole<Transaction> {
|
||||
async initWithLast(): Promise<this> {
|
||||
this.self = await this.createQueryForPendingItems(
|
||||
Transaction.createQueryBuilder(),
|
||||
'Transaction.id = dltTransaction.transactionId',
|
||||
// eslint-disable-next-line camelcase
|
||||
{ balance_date: 'ASC', Transaction_id: 'ASC' },
|
||||
)
|
||||
return this
|
||||
}
|
||||
|
||||
public getTimestamp(): number {
|
||||
if (!this.self) {
|
||||
return Infinity
|
||||
}
|
||||
return this.self.balanceDate.getTime()
|
||||
}
|
||||
|
||||
public convertToGraphqlInput(): TransactionDraft | UserAccountDraft {
|
||||
if (!this.self) {
|
||||
throw new LogError('try to create dlt entry for empty transaction')
|
||||
}
|
||||
return new TransactionDraft(this.self)
|
||||
}
|
||||
|
||||
protected setJoinId(dltTransaction: DltTransaction): void {
|
||||
if (!this.self) {
|
||||
throw new LogError('try to create dlt entry for empty transaction')
|
||||
}
|
||||
dltTransaction.transactionId = this.self.id
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,45 @@
|
||||
import { DltTransaction } from '@entity/DltTransaction'
|
||||
import { User } from '@entity/User'
|
||||
|
||||
import { TransactionDraft } from '@dltConnector/model/TransactionDraft'
|
||||
import { UserAccountDraft } from '@dltConnector/model/UserAccountDraft'
|
||||
|
||||
import { LogError } from '@/server/LogError'
|
||||
|
||||
import { AbstractTransactionToDltRole } from './AbstractTransactionToDlt.role'
|
||||
|
||||
/**
|
||||
* send new user to dlt connector, will be made to RegisterAddress Transaction
|
||||
*/
|
||||
export class UserToDltRole extends AbstractTransactionToDltRole<User> {
|
||||
async initWithLast(): Promise<this> {
|
||||
this.self = await this.createQueryForPendingItems(
|
||||
User.createQueryBuilder(),
|
||||
'User.id = dltTransaction.userId',
|
||||
// eslint-disable-next-line camelcase
|
||||
{ User_created_at: 'ASC', User_id: 'ASC' },
|
||||
)
|
||||
return this
|
||||
}
|
||||
|
||||
public getTimestamp(): number {
|
||||
if (!this.self) {
|
||||
return Infinity
|
||||
}
|
||||
return this.self.createdAt.getTime()
|
||||
}
|
||||
|
||||
public convertToGraphqlInput(): TransactionDraft | UserAccountDraft {
|
||||
if (!this.self) {
|
||||
throw new LogError('try to create dlt entry for empty transaction')
|
||||
}
|
||||
return new UserAccountDraft(this.self)
|
||||
}
|
||||
|
||||
protected setJoinId(dltTransaction: DltTransaction): void {
|
||||
if (!this.self) {
|
||||
throw new LogError('try to create dlt entry for empty user')
|
||||
}
|
||||
dltTransaction.userId = this.self.id
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,65 @@
|
||||
import { Transaction } from '@entity/Transaction'
|
||||
import { TransactionLink } from '@entity/TransactionLink'
|
||||
import { User } from '@entity/User'
|
||||
// eslint-disable-next-line import/named, n/no-extraneous-import
|
||||
import { FetchError } from 'node-fetch'
|
||||
|
||||
import { DltConnectorClient } from '@/apis/dltConnector/DltConnectorClient'
|
||||
import { TransactionResult } from '@/apis/dltConnector/model/TransactionResult'
|
||||
|
||||
import { AbstractTransactionToDltRole } from './AbstractTransactionToDlt.role'
|
||||
import { TransactionLinkToDltRole } from './TransactionLinkToDlt.role'
|
||||
import { TransactionToDltRole } from './TransactionToDlt.role'
|
||||
import { UserToDltRole } from './UserToDlt.role'
|
||||
|
||||
/**
|
||||
* @DCI-Context
|
||||
* Context for sending transactions to dlt connector, always the oldest not sended transaction first
|
||||
*/
|
||||
export async function transactionToDlt(dltConnector: DltConnectorClient): Promise<void> {
|
||||
async function findNextPendingTransaction(): Promise<
|
||||
AbstractTransactionToDltRole<Transaction | User | TransactionLink>
|
||||
> {
|
||||
// collect each oldest not sended entity from db and choose oldest
|
||||
const results = await Promise.all([
|
||||
new TransactionToDltRole().initWithLast(),
|
||||
new UserToDltRole().initWithLast(),
|
||||
new TransactionLinkToDltRole().initWithLast(),
|
||||
])
|
||||
|
||||
// sort array to get oldest at first place
|
||||
results.sort((a, b) => {
|
||||
return a.getTimestamp() - b.getTimestamp()
|
||||
})
|
||||
return results[0]
|
||||
}
|
||||
|
||||
while (true) {
|
||||
const pendingTransactionRole = await findNextPendingTransaction()
|
||||
const pendingTransaction = pendingTransactionRole.getEntity()
|
||||
if (!pendingTransaction) {
|
||||
break
|
||||
}
|
||||
let result: TransactionResult | undefined
|
||||
let messageId = ''
|
||||
let error: string | null = null
|
||||
|
||||
try {
|
||||
result = await dltConnector.transmitTransaction(
|
||||
pendingTransactionRole.convertToGraphqlInput(),
|
||||
)
|
||||
if (result?.succeed && result.recipe) {
|
||||
messageId = result.recipe.messageIdHex
|
||||
} else {
|
||||
error = 'skipped'
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof FetchError) {
|
||||
throw e
|
||||
}
|
||||
error = e instanceof Error ? e.message : String(e)
|
||||
}
|
||||
|
||||
await pendingTransactionRole.saveTransactionResult(messageId, error)
|
||||
}
|
||||
}
|
||||
@ -1,21 +1,18 @@
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import { backendLogger as logger } from '@/server/logger'
|
||||
import { BaseEntity, EntityPropertyNotFoundError, EntityTarget, OrderByCondition, SelectQueryBuilder } from '@dbTools/typeorm'
|
||||
import { DltTransaction } from '@entity/DltTransaction'
|
||||
import { Transaction } from '@entity/Transaction'
|
||||
import { TransactionLink } from '@entity/TransactionLink'
|
||||
import { User } from '@entity/User'
|
||||
import { EntityPropertyNotFoundError } from '@dbTools/typeorm'
|
||||
// eslint-disable-next-line import/named, n/no-extraneous-import
|
||||
import { FetchError } from 'node-fetch'
|
||||
|
||||
import { DltConnectorClient } from '@dltConnector/DltConnectorClient'
|
||||
|
||||
import { TransactionResult } from '@/apis/dltConnector/model/TransactionResult'
|
||||
import { LogError } from '@/server/LogError'
|
||||
import { backendLogger as logger } from '@/server/logger'
|
||||
import {
|
||||
InterruptiveSleepManager,
|
||||
TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY,
|
||||
} from '@/util/InterruptiveSleepManager'
|
||||
import { LogError } from '@/server/LogError'
|
||||
|
||||
import { transactionToDlt } from './interaction/transactionToDlt/transactionToDlt.context'
|
||||
|
||||
let isLoopRunning = true
|
||||
|
||||
@ -23,127 +20,6 @@ export const stopSendTransactionsToDltConnector = (): void => {
|
||||
isLoopRunning = false
|
||||
}
|
||||
|
||||
interface NextPendingTransactionQueries {
|
||||
lastTransactionQuery: SelectQueryBuilder<Transaction>
|
||||
lastUserQuery: SelectQueryBuilder<User>
|
||||
lastTransactionLinkQuery: SelectQueryBuilder<TransactionLink>
|
||||
}
|
||||
|
||||
function logTransactionResult(data: { id: number; messageId: string; error: string | null }): void {
|
||||
if (data.error) {
|
||||
logger.error(`Store dltTransaction with error: id=${data.id}, error=${data.error}`)
|
||||
} else {
|
||||
logger.info(`Store dltTransaction: messageId=${data.messageId}, id=${data.id}`)
|
||||
}
|
||||
}
|
||||
|
||||
async function saveTransactionResult(
|
||||
pendingTransaction: User | Transaction | TransactionLink,
|
||||
messageId: string,
|
||||
error: string | null,
|
||||
): Promise<void> {
|
||||
const dltTransaction = DltTransaction.create()
|
||||
dltTransaction.messageId = messageId
|
||||
dltTransaction.error = error
|
||||
if (pendingTransaction instanceof User) {
|
||||
dltTransaction.userId = pendingTransaction.id
|
||||
} else if (pendingTransaction instanceof Transaction) {
|
||||
dltTransaction.transactionId = pendingTransaction.id
|
||||
} else if (pendingTransaction instanceof TransactionLink) {
|
||||
dltTransaction.transactionLinkId = pendingTransaction.id
|
||||
}
|
||||
await DltTransaction.save(dltTransaction)
|
||||
logTransactionResult(dltTransaction)
|
||||
}
|
||||
|
||||
async function findNextPendingTransaction(): Promise<Transaction | User | TransactionLink | null> {
|
||||
// Helper function to avoid code repetition
|
||||
const createQueryForPendingItems = (
|
||||
qb: SelectQueryBuilder<Transaction | User | TransactionLink>,
|
||||
joinCondition: string,
|
||||
orderBy: OrderByCondition,
|
||||
): Promise<Transaction | User | TransactionLink | null> => {
|
||||
return qb
|
||||
.leftJoin(DltTransaction, 'dltTransaction', joinCondition)
|
||||
.where('dltTransaction.user_id IS NULL')
|
||||
.andWhere('dltTransaction.transaction_id IS NULL')
|
||||
.andWhere('dltTransaction.transaction_link_Id IS NULL')
|
||||
.orderBy(orderBy)
|
||||
.limit(1)
|
||||
.getOne()
|
||||
}
|
||||
|
||||
const lastTransactionPromise = createQueryForPendingItems(
|
||||
Transaction.createQueryBuilder(),
|
||||
'Transaction.id = dltTransaction.transactionId',
|
||||
// eslint-disable-next-line camelcase
|
||||
{ balance_date: 'ASC', Transaction_id: 'ASC' },
|
||||
)
|
||||
|
||||
const lastUserPromise = createQueryForPendingItems(
|
||||
User.createQueryBuilder(),
|
||||
'User.id = dltTransaction.userId',
|
||||
// eslint-disable-next-line camelcase
|
||||
{ User_created_at: 'ASC', User_id: 'ASC' },
|
||||
)
|
||||
|
||||
const lastTransactionLinkPromise = createQueryForPendingItems(
|
||||
TransactionLink.createQueryBuilder().leftJoinAndSelect('transactionLink.user', 'user'),
|
||||
'TransactionLink.id = dltTransaction.transactionLinkId',
|
||||
// eslint-disable-next-line camelcase
|
||||
{ TransactionLinkId_created_at: 'ASC', User_id: 'ASC' },
|
||||
)
|
||||
|
||||
const results = await Promise.all([
|
||||
lastTransactionPromise,
|
||||
lastUserPromise,
|
||||
lastTransactionLinkPromise,
|
||||
])
|
||||
|
||||
results.sort((a, b) => {
|
||||
const getTime = (input: Transaction | User | TransactionLink | null) => {
|
||||
if (!input) return Infinity
|
||||
if (input instanceof Transaction) {
|
||||
return input.balanceDate.getTime()
|
||||
} else if (input instanceof User || input instanceof TransactionLink) {
|
||||
return input.createdAt.getTime()
|
||||
}
|
||||
return Infinity
|
||||
}
|
||||
return getTime(a) - getTime(b)
|
||||
})
|
||||
return results[0] ?? null
|
||||
}
|
||||
|
||||
async function processPendingTransactions(dltConnector: DltConnectorClient): Promise<void> {
|
||||
let pendingTransaction: Transaction | User | TransactionLink | null = null
|
||||
do {
|
||||
pendingTransaction = await findNextPendingTransaction()
|
||||
if (!pendingTransaction) {
|
||||
return
|
||||
}
|
||||
let result: TransactionResult | undefined
|
||||
let messageId = ''
|
||||
let error: string | null = null
|
||||
|
||||
try {
|
||||
result = await dltConnector.transmitTransaction(pendingTransaction)
|
||||
if (result?.succeed && result.recipe) {
|
||||
messageId = result.recipe.messageIdHex
|
||||
} else {
|
||||
error = 'skipped'
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof FetchError) {
|
||||
throw e
|
||||
}
|
||||
error = e instanceof Error ? e.message : String(e)
|
||||
}
|
||||
|
||||
await saveTransactionResult(pendingTransaction, messageId, error)
|
||||
} while (pendingTransaction)
|
||||
}
|
||||
|
||||
export async function sendTransactionsToDltConnector(): Promise<void> {
|
||||
const dltConnector = DltConnectorClient.getInstance()
|
||||
|
||||
@ -161,7 +37,7 @@ export async function sendTransactionsToDltConnector(): Promise<void> {
|
||||
while (isLoopRunning) {
|
||||
try {
|
||||
// return after no pending transactions are left
|
||||
await processPendingTransactions(dltConnector)
|
||||
await transactionToDlt(dltConnector)
|
||||
await InterruptiveSleepManager.getInstance().sleep(
|
||||
TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY,
|
||||
1000,
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import { delay } from './utilities'
|
||||
|
||||
/**
|
||||
* Sleep, that can be interrupted
|
||||
* call sleep only for msSteps and than check if interrupt was called
|
||||
@ -14,17 +16,11 @@ export class InterruptiveSleep {
|
||||
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)
|
||||
await delay(this.msSteps)
|
||||
waited += this.msSteps
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import { promisify } from 'util'
|
||||
|
||||
import { Decimal } from 'decimal.js-light'
|
||||
import i18n from 'i18n'
|
||||
|
||||
@ -30,6 +32,8 @@ export function resetInterface<T extends Record<string, any>>(obj: T): T {
|
||||
return obj
|
||||
}
|
||||
|
||||
export const delay = promisify(setTimeout)
|
||||
|
||||
export const ensureUrlEndsWithSlash = (url: string): string => {
|
||||
return url.endsWith('/') ? url : url.concat('/')
|
||||
}
|
||||
|
||||
9
dlt-connector/src/graphql/input/IdentifierSeed.ts
Normal file
9
dlt-connector/src/graphql/input/IdentifierSeed.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { IsString } from 'class-validator'
|
||||
import { InputType, Field } from 'type-graphql'
|
||||
|
||||
@InputType()
|
||||
export class IdentifierSeed {
|
||||
@Field(() => String)
|
||||
@IsString()
|
||||
seed: string
|
||||
}
|
||||
@ -5,6 +5,7 @@ import { InputType, Field } from 'type-graphql'
|
||||
import { InputTransactionType } from '@enum/InputTransactionType'
|
||||
import { isValidDateString, isValidNumberString } from '@validator/DateString'
|
||||
|
||||
import { IdentifierSeed } from './IdentifierSeed'
|
||||
import { UserIdentifier } from './UserIdentifier'
|
||||
|
||||
@InputType()
|
||||
@ -17,7 +18,7 @@ export class TransactionDraft {
|
||||
@Field(() => UserIdentifier)
|
||||
@IsObject()
|
||||
@ValidateNested()
|
||||
linkedUser: UserIdentifier
|
||||
linkedUser: UserIdentifier | IdentifierSeed
|
||||
|
||||
@Field(() => String)
|
||||
@isValidNumberString()
|
||||
|
||||
@ -1,12 +1,13 @@
|
||||
import { KeyPairEd25519 } from 'gradido-blockchain-js'
|
||||
|
||||
import { IdentifierSeed } from '@/graphql/input/IdentifierSeed'
|
||||
import { UserIdentifier } from '@/graphql/input/UserIdentifier'
|
||||
import { KeyPairCacheManager } from '@/manager/KeyPairCacheManager'
|
||||
|
||||
import { AbstractRemoteKeyPairRole } from './AbstractRemoteKeyPair.role'
|
||||
import { AccountKeyPairRole } from './AccountKeyPair.role'
|
||||
import { ForeignCommunityKeyPairRole } from './ForeignCommunityKeyPair.role'
|
||||
import { HomeCommunityKeyPairRole } from './HomeCommunityKeyPair.role'
|
||||
import { LinkedTransactionKeyPairRole } from './LinkedTransactionKeyPair.role'
|
||||
import { RemoteAccountKeyPairRole } from './RemoteAccountKeyPair.role'
|
||||
import { UserKeyPairRole } from './UserKeyPair.role'
|
||||
|
||||
@ -14,43 +15,49 @@ import { UserKeyPairRole } from './UserKeyPair.role'
|
||||
* @DCI-Context
|
||||
* Context for calculating key pair for signing transactions
|
||||
*/
|
||||
export async function KeyPairCalculation(input: UserIdentifier | string): Promise<KeyPairEd25519> {
|
||||
export async function KeyPairCalculation(
|
||||
input: UserIdentifier | string | IdentifierSeed,
|
||||
): Promise<KeyPairEd25519> {
|
||||
const cache = KeyPairCacheManager.getInstance()
|
||||
const keyPair = cache.findKeyPair(input)
|
||||
|
||||
// Try cache lookup first
|
||||
let keyPair = cache.findKeyPair(input)
|
||||
if (keyPair) {
|
||||
return keyPair
|
||||
}
|
||||
let communityUUID: string
|
||||
if (input instanceof UserIdentifier) {
|
||||
communityUUID = input.communityUuid
|
||||
} else {
|
||||
communityUUID = input
|
||||
}
|
||||
|
||||
if (cache.getHomeCommunityUUID() !== communityUUID) {
|
||||
// it isn't home community so we can only retrieve public keys
|
||||
let role: AbstractRemoteKeyPairRole
|
||||
if (input instanceof UserIdentifier) {
|
||||
role = new RemoteAccountKeyPairRole(input)
|
||||
} else {
|
||||
role = new ForeignCommunityKeyPairRole(input)
|
||||
const retrieveKeyPair = async (
|
||||
input: UserIdentifier | string | IdentifierSeed,
|
||||
): Promise<KeyPairEd25519> => {
|
||||
if (input instanceof IdentifierSeed) {
|
||||
return new LinkedTransactionKeyPairRole(input.seed).generateKeyPair()
|
||||
}
|
||||
const keyPair = await role.retrieveKeyPair()
|
||||
cache.addKeyPair(input, keyPair)
|
||||
return keyPair
|
||||
|
||||
const communityUUID = input instanceof UserIdentifier ? input.communityUuid : input
|
||||
|
||||
// If input does not belong to the home community, handle as remote key pair
|
||||
if (cache.getHomeCommunityUUID() !== communityUUID) {
|
||||
const role =
|
||||
input instanceof UserIdentifier
|
||||
? new RemoteAccountKeyPairRole(input)
|
||||
: new ForeignCommunityKeyPairRole(input)
|
||||
return await role.retrieveKeyPair()
|
||||
}
|
||||
|
||||
let communityKeyPair = cache.findKeyPair(communityUUID)
|
||||
if (!communityKeyPair) {
|
||||
communityKeyPair = new HomeCommunityKeyPairRole().generateKeyPair()
|
||||
cache.addKeyPair(communityUUID, communityKeyPair)
|
||||
}
|
||||
if (input instanceof UserIdentifier) {
|
||||
const userKeyPair = new UserKeyPairRole(input, communityKeyPair).generateKeyPair()
|
||||
const accountNr = input.accountNr ?? 1
|
||||
return new AccountKeyPairRole(accountNr, userKeyPair).generateKeyPair()
|
||||
}
|
||||
return communityKeyPair
|
||||
}
|
||||
|
||||
let communityKeyPair = cache.findKeyPair(communityUUID)
|
||||
if (!communityKeyPair) {
|
||||
communityKeyPair = new HomeCommunityKeyPairRole().generateKeyPair()
|
||||
cache.addKeyPair(communityUUID, communityKeyPair)
|
||||
}
|
||||
if (input instanceof UserIdentifier) {
|
||||
const userKeyPair = new UserKeyPairRole(input, communityKeyPair).generateKeyPair()
|
||||
const accountNr = input.accountNr ?? 1
|
||||
const accountKeyPair = new AccountKeyPairRole(accountNr, userKeyPair).generateKeyPair()
|
||||
cache.addKeyPair(input, accountKeyPair)
|
||||
return accountKeyPair
|
||||
}
|
||||
return communityKeyPair
|
||||
keyPair = await retrieveKeyPair(input)
|
||||
cache.addKeyPair(input, keyPair)
|
||||
return keyPair
|
||||
}
|
||||
|
||||
@ -0,0 +1,22 @@
|
||||
import { KeyPairEd25519, MemoryBlock } from 'gradido-blockchain-js'
|
||||
|
||||
import { LogError } from '@/server/LogError'
|
||||
|
||||
import { AbstractKeyPairRole } from './AbstractKeyPair.role'
|
||||
|
||||
export class LinkedTransactionKeyPairRole extends AbstractKeyPairRole {
|
||||
public constructor(private seed: string) {
|
||||
super()
|
||||
}
|
||||
|
||||
public generateKeyPair(): KeyPairEd25519 {
|
||||
// seed is expected to be 24 bytes long, but we need 32
|
||||
// so hash the seed with blake2 and we have 32 Bytes
|
||||
const hash = new MemoryBlock(this.seed).calculateHash()
|
||||
const keyPair = KeyPairEd25519.create(hash)
|
||||
if (!keyPair) {
|
||||
throw new LogError('error creating Ed25519 KeyPair from seed', this.seed)
|
||||
}
|
||||
return keyPair
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
import { GradidoTransactionBuilder, TransferAmount } from 'gradido-blockchain-js'
|
||||
|
||||
import { IdentifierSeed } from '@/graphql/input/IdentifierSeed'
|
||||
import { TransactionDraft } from '@/graphql/input/TransactionDraft'
|
||||
import { LogError } from '@/server/LogError'
|
||||
|
||||
@ -21,12 +22,16 @@ export class CreationTransactionRole extends AbstractTransactionRole {
|
||||
}
|
||||
|
||||
public async getGradidoTransactionBuilder(): Promise<GradidoTransactionBuilder> {
|
||||
const builder = new GradidoTransactionBuilder()
|
||||
const recipientKeyPair = await KeyPairCalculation(this.self.user)
|
||||
const signerKeyPair = await KeyPairCalculation(this.self.linkedUser)
|
||||
if (this.self.linkedUser instanceof IdentifierSeed) {
|
||||
throw new LogError('invalid recipient, it is a IdentifierSeed instead of a UserIdentifier')
|
||||
}
|
||||
if (!this.self.targetDate) {
|
||||
throw new LogError('target date missing for creation transaction')
|
||||
}
|
||||
const builder = new GradidoTransactionBuilder()
|
||||
const recipientKeyPair = await KeyPairCalculation(this.self.user)
|
||||
const signerKeyPair = await KeyPairCalculation(this.self.linkedUser)
|
||||
|
||||
builder
|
||||
.setCreatedAt(new Date(this.self.createdAt))
|
||||
.setMemo('dummy memo for creation')
|
||||
|
||||
@ -1,20 +1,28 @@
|
||||
import { GradidoTransactionBuilder, GradidoTransfer, TransferAmount } from 'gradido-blockchain-js'
|
||||
|
||||
import { UserIdentifier } from '@/graphql/input/UserIdentifier'
|
||||
import { LogError } from '@/server/LogError'
|
||||
import { uuid4ToHash } from '@/utils/typeConverter'
|
||||
|
||||
import { KeyPairCalculation } from '../keyPairCalculation/KeyPairCalculation.context'
|
||||
|
||||
import { TransferTransactionRole } from './TransferTransaction.role'
|
||||
|
||||
export class DeferredTransferTransactionRole extends TransferTransactionRole {
|
||||
getRecipientCommunityUuid(): string {
|
||||
throw new LogError('cannot be used as cross group transaction')
|
||||
}
|
||||
|
||||
public async getGradidoTransactionBuilder(): Promise<GradidoTransactionBuilder> {
|
||||
const builder = new GradidoTransactionBuilder()
|
||||
const senderKeyPair = await KeyPairCalculation(this.self.user)
|
||||
const recipientKeyPair = await KeyPairCalculation(this.self.linkedUser)
|
||||
if (this.self.linkedUser instanceof UserIdentifier) {
|
||||
throw new LogError('invalid recipient, it is a UserIdentifier instead of a IdentifierSeed')
|
||||
}
|
||||
if (!this.self.timeoutDate) {
|
||||
throw new LogError('timeoutDate date missing for deferred transfer transaction')
|
||||
}
|
||||
const builder = new GradidoTransactionBuilder()
|
||||
const senderKeyPair = await KeyPairCalculation(this.self.user)
|
||||
const recipientKeyPair = await KeyPairCalculation(this.self.linkedUser)
|
||||
|
||||
builder
|
||||
.setCreatedAt(new Date(this.self.createdAt))
|
||||
.setMemo('dummy memo for transfer')
|
||||
@ -25,14 +33,6 @@ export class DeferredTransferTransactionRole extends TransferTransactionRole {
|
||||
),
|
||||
new Date(this.self.timeoutDate),
|
||||
)
|
||||
const senderCommunity = this.self.user.communityUuid
|
||||
const recipientCommunity = this.self.linkedUser.communityUuid
|
||||
if (senderCommunity !== recipientCommunity) {
|
||||
// we have a cross group transaction
|
||||
builder
|
||||
.setSenderCommunity(uuid4ToHash(senderCommunity).convertToHex())
|
||||
.setRecipientCommunity(uuid4ToHash(recipientCommunity).convertToHex())
|
||||
}
|
||||
builder.sign(senderKeyPair)
|
||||
return builder
|
||||
}
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import { GradidoTransactionBuilder, TransferAmount } from 'gradido-blockchain-js'
|
||||
|
||||
import { IdentifierSeed } from '@/graphql/input/IdentifierSeed'
|
||||
import { TransactionDraft } from '@/graphql/input/TransactionDraft'
|
||||
import { LogError } from '@/server/LogError'
|
||||
import { uuid4ToHash } from '@/utils/typeConverter'
|
||||
|
||||
import { KeyPairCalculation } from '../keyPairCalculation/KeyPairCalculation.context'
|
||||
@ -17,10 +19,17 @@ export class TransferTransactionRole extends AbstractTransactionRole {
|
||||
}
|
||||
|
||||
getRecipientCommunityUuid(): string {
|
||||
if (this.self.linkedUser instanceof IdentifierSeed) {
|
||||
throw new LogError('invalid recipient, it is a IdentifierSeed instead of a UserIdentifier')
|
||||
}
|
||||
return this.self.linkedUser.communityUuid
|
||||
}
|
||||
|
||||
public async getGradidoTransactionBuilder(): Promise<GradidoTransactionBuilder> {
|
||||
if (this.self.linkedUser instanceof IdentifierSeed) {
|
||||
throw new LogError('invalid recipient, it is a IdentifierSeed instead of a UserIdentifier')
|
||||
}
|
||||
|
||||
const builder = new GradidoTransactionBuilder()
|
||||
const senderKeyPair = await KeyPairCalculation(this.self.user)
|
||||
const recipientKeyPair = await KeyPairCalculation(this.self.linkedUser)
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { InputTransactionType } from '@/graphql/enum/InputTransactionType'
|
||||
import { TransactionDraft } from '@/graphql/input/TransactionDraft'
|
||||
import { UserIdentifier } from '@/graphql/input/UserIdentifier'
|
||||
import { getEnumValue } from '@/utils/typeConverter'
|
||||
|
||||
import { AbstractLoggingView } from './AbstractLogging.view'
|
||||
@ -14,7 +15,10 @@ export class TransactionDraftLoggingView extends AbstractLoggingView {
|
||||
public toJSON(): any {
|
||||
return {
|
||||
user: new UserIdentifierLoggingView(this.self.user).toJSON(),
|
||||
linkedUser: new UserIdentifierLoggingView(this.self.linkedUser).toJSON(),
|
||||
linkedUser:
|
||||
this.self.linkedUser instanceof UserIdentifier
|
||||
? new UserIdentifierLoggingView(this.self.linkedUser).toJSON()
|
||||
: 'seed',
|
||||
amount: Number(this.self.amount),
|
||||
type: getEnumValue(InputTransactionType, this.self.type),
|
||||
createdAt: this.self.createdAt,
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { KeyPairEd25519 } from 'gradido-blockchain-js'
|
||||
|
||||
import { IdentifierSeed } from '@/graphql/input/IdentifierSeed'
|
||||
import { UserIdentifier } from '@/graphql/input/UserIdentifier'
|
||||
import { LogError } from '@/server/LogError'
|
||||
|
||||
@ -44,11 +45,14 @@ export class KeyPairCacheManager {
|
||||
return this.homeCommunityUUID
|
||||
}
|
||||
|
||||
public findKeyPair(input: UserIdentifier | string): KeyPairEd25519 | undefined {
|
||||
public findKeyPair(input: UserIdentifier | string | IdentifierSeed): KeyPairEd25519 | undefined {
|
||||
return this.cache.get(this.getKey(input))
|
||||
}
|
||||
|
||||
public addKeyPair(input: UserIdentifier | string, keyPair: KeyPairEd25519): void {
|
||||
public addKeyPair(
|
||||
input: UserIdentifier | string | IdentifierSeed,
|
||||
keyPair: KeyPairEd25519,
|
||||
): void {
|
||||
const key = this.getKey(input)
|
||||
if (this.cache.has(key)) {
|
||||
throw new LogError('key already exist, cannot add', key)
|
||||
@ -56,9 +60,11 @@ export class KeyPairCacheManager {
|
||||
this.cache.set(key, keyPair)
|
||||
}
|
||||
|
||||
protected getKey(input: UserIdentifier | string): string {
|
||||
protected getKey(input: UserIdentifier | string | IdentifierSeed): string {
|
||||
if (input instanceof UserIdentifier) {
|
||||
return input.uuid
|
||||
} else if (input instanceof IdentifierSeed) {
|
||||
return input.seed
|
||||
} else {
|
||||
return input
|
||||
}
|
||||
|
||||
@ -1082,7 +1082,7 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.5.tgz#1ef302e01cf7d2b5a0fa526790c9123bf1d06690"
|
||||
integrity sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==
|
||||
|
||||
"@types/node-fetch@^2.6.1", "@types/node-fetch@^2.6.11":
|
||||
"@types/node-fetch@^2.6.1":
|
||||
version "2.6.11"
|
||||
resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.11.tgz#9b39b78665dae0e82a08f02f4967d62c66f95d24"
|
||||
integrity sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==
|
||||
@ -2112,11 +2112,6 @@ cssstyle@^2.3.0:
|
||||
dependencies:
|
||||
cssom "~0.3.6"
|
||||
|
||||
data-uri-to-buffer@^4.0.0:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz#d8feb2b2881e6a4f58c2e08acfd0e2834e26222e"
|
||||
integrity sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==
|
||||
|
||||
data-urls@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-2.0.0.tgz#156485a72963a970f5d5821aaf642bef2bf2db9b"
|
||||
@ -2936,14 +2931,6 @@ fb-watchman@^2.0.0:
|
||||
dependencies:
|
||||
bser "2.1.1"
|
||||
|
||||
fetch-blob@^3.1.2, fetch-blob@^3.1.4:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/fetch-blob/-/fetch-blob-3.2.0.tgz#f09b8d4bbd45adc6f0c20b7e787e793e309dcce9"
|
||||
integrity sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==
|
||||
dependencies:
|
||||
node-domexception "^1.0.0"
|
||||
web-streams-polyfill "^3.0.3"
|
||||
|
||||
figures@^3.0.0:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af"
|
||||
@ -3063,13 +3050,6 @@ form-data@^4.0.0:
|
||||
combined-stream "^1.0.8"
|
||||
mime-types "^2.1.12"
|
||||
|
||||
formdata-polyfill@^4.0.10:
|
||||
version "4.0.10"
|
||||
resolved "https://registry.yarnpkg.com/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz#24807c31c9d402e002ab3d8c720144ceb8848423"
|
||||
integrity sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==
|
||||
dependencies:
|
||||
fetch-blob "^3.1.2"
|
||||
|
||||
forwarded@0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811"
|
||||
@ -4690,11 +4670,6 @@ node-api-headers@^1.1.0:
|
||||
resolved "https://registry.yarnpkg.com/node-api-headers/-/node-api-headers-1.3.0.tgz#bb32c6b3e33fb0004bd93c66787bf00998c834ea"
|
||||
integrity sha512-8Bviwtw4jNhv0B2qDjj4M5e6GyAuGtxsmZTrFJu3S3Z0+oHwIgSUdIKkKJmZd+EbMo7g3v4PLBbrjxwmZOqMBg==
|
||||
|
||||
node-domexception@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5"
|
||||
integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==
|
||||
|
||||
node-fetch@^2.6.12, node-fetch@^2.6.7:
|
||||
version "2.7.0"
|
||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d"
|
||||
@ -4702,15 +4677,6 @@ node-fetch@^2.6.12, node-fetch@^2.6.7:
|
||||
dependencies:
|
||||
whatwg-url "^5.0.0"
|
||||
|
||||
node-fetch@^3.3.2:
|
||||
version "3.3.2"
|
||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.3.2.tgz#d1e889bacdf733b4ff3b2b243eb7a12866a0b78b"
|
||||
integrity sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==
|
||||
dependencies:
|
||||
data-uri-to-buffer "^4.0.0"
|
||||
fetch-blob "^3.1.4"
|
||||
formdata-polyfill "^4.0.10"
|
||||
|
||||
node-gyp-build@^4.8.1:
|
||||
version "4.8.2"
|
||||
resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.8.2.tgz#4f802b71c1ab2ca16af830e6c1ea7dd1ad9496fa"
|
||||
@ -6299,11 +6265,6 @@ walker@^1.0.7:
|
||||
dependencies:
|
||||
makeerror "1.0.12"
|
||||
|
||||
web-streams-polyfill@^3.0.3:
|
||||
version "3.3.3"
|
||||
resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz#2073b91a2fdb1fbfbd401e7de0ac9f8214cecb4b"
|
||||
integrity sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==
|
||||
|
||||
webidl-conversions@^3.0.0:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user