move code into interaction in backend, implement transaction link in dlt-connector

This commit is contained in:
einhornimmond 2024-11-13 14:22:04 +01:00
parent 4875621699
commit e691db05b7
20 changed files with 400 additions and 248 deletions

View File

@ -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()),

View File

@ -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) {

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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)
}
}

View File

@ -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,

View File

@ -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
}
}

View File

@ -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('/')
}

View 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
}

View File

@ -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()

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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')

View File

@ -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
}

View File

@ -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)

View File

@ -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,

View File

@ -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
}

View File

@ -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"