mirror of
https://github.com/IT4Change/gradido.git
synced 2026-02-06 09:56:05 +00:00
244 lines
7.5 KiB
TypeScript
244 lines
7.5 KiB
TypeScript
import { Transaction as DbTransaction } from '@entity/Transaction'
|
|
import { User as DbUser } from '@entity/User'
|
|
import { gql, GraphQLClient } from 'graphql-request'
|
|
|
|
import { CONFIG } from '@/config'
|
|
import { TransactionTypeId } from '@/graphql/enum/TransactionTypeId'
|
|
import { LogError } from '@/server/LogError'
|
|
import { backendLogger as logger } from '@/server/logger'
|
|
import { Contribution } from '@entity/Contribution'
|
|
|
|
const sendTransaction = gql`
|
|
mutation ($input: TransactionInput!) {
|
|
sendTransaction(data: $input) {
|
|
dltTransactionIdHex
|
|
}
|
|
}
|
|
`
|
|
|
|
enum TransactionType {
|
|
GRADIDO_TRANSFER = 1,
|
|
GRADIDO_CREATION = 2,
|
|
GROUP_FRIENDS_UPDATE = 3,
|
|
REGISTER_ADDRESS = 4,
|
|
GRADIDO_DEFERRED_TRANSFER = 5,
|
|
COMMUNITY_ROOT = 6,
|
|
}
|
|
|
|
enum TransactionErrorType {
|
|
NOT_IMPLEMENTED_YET = 'Not Implemented yet',
|
|
MISSING_PARAMETER = 'Missing parameter',
|
|
ALREADY_EXIST = 'Already exist',
|
|
DB_ERROR = 'DB Error',
|
|
PROTO_DECODE_ERROR = 'Proto Decode Error',
|
|
PROTO_ENCODE_ERROR = 'Proto Encode Error',
|
|
INVALID_SIGNATURE = 'Invalid Signature',
|
|
LOGIC_ERROR = 'Logic Error',
|
|
NOT_FOUND = 'Not found',
|
|
}
|
|
|
|
interface TransactionError {
|
|
type: TransactionErrorType
|
|
message: string
|
|
name: string
|
|
}
|
|
|
|
interface TransactionRecipe {
|
|
id: number
|
|
createdAt: string
|
|
type: TransactionType
|
|
topic: string
|
|
}
|
|
|
|
interface TransactionResult {
|
|
error?: TransactionError
|
|
recipe?: TransactionRecipe
|
|
succeed: boolean
|
|
}
|
|
|
|
interface UserIdentifier {
|
|
uuid: string
|
|
communityUuid: string
|
|
accountNr?: number
|
|
}
|
|
|
|
// from ChatGPT
|
|
function getTransactionTypeString(id: TransactionTypeId): string {
|
|
const key = Object.keys(TransactionTypeId).find(
|
|
(key) => TransactionTypeId[key as keyof typeof TransactionTypeId] === id,
|
|
)
|
|
if (key === undefined) {
|
|
throw new LogError('invalid transaction type id: ' + id.toString())
|
|
}
|
|
return key
|
|
}
|
|
|
|
// 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 DltConnectorClient {
|
|
// eslint-disable-next-line no-use-before-define
|
|
private static instance: DltConnectorClient
|
|
client: GraphQLClient
|
|
/**
|
|
* 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(): DltConnectorClient | undefined {
|
|
if (!CONFIG.DLT_CONNECTOR || !CONFIG.DLT_CONNECTOR_URL) {
|
|
logger.info(`dlt-connector are disabled via config...`)
|
|
return
|
|
}
|
|
if (!DltConnectorClient.instance) {
|
|
DltConnectorClient.instance = new DltConnectorClient()
|
|
}
|
|
if (!DltConnectorClient.instance.client) {
|
|
try {
|
|
DltConnectorClient.instance.client = new GraphQLClient(CONFIG.DLT_CONNECTOR_URL, {
|
|
method: 'GET',
|
|
jsonSerializer: {
|
|
parse: JSON.parse,
|
|
stringify: JSON.stringify,
|
|
},
|
|
})
|
|
} catch (e) {
|
|
logger.error("couldn't connect to dlt-connector: ", e)
|
|
return
|
|
}
|
|
}
|
|
return DltConnectorClient.instance
|
|
}
|
|
|
|
protected async getCorrectUserUUID(
|
|
transaction: DbTransaction,
|
|
type: 'sender' | 'recipient',
|
|
): Promise<string> {
|
|
let confirmingUserId: number | undefined
|
|
logger.info('confirming user id', confirmingUserId)
|
|
switch (transaction.typeId) {
|
|
case TransactionTypeId.CREATION:
|
|
confirmingUserId = (
|
|
await Contribution.findOneOrFail({ where: { transactionId: transaction.id } })
|
|
).confirmedBy
|
|
if (!confirmingUserId) {
|
|
throw new LogError(
|
|
"couldn't find id of confirming moderator for contribution transaction!",
|
|
)
|
|
}
|
|
if (type === 'sender') {
|
|
return (await DbUser.findOneOrFail({ where: { id: confirmingUserId } })).gradidoID
|
|
} else if (type === 'recipient') {
|
|
return transaction.userGradidoID
|
|
}
|
|
break
|
|
case TransactionTypeId.SEND:
|
|
if (type === 'sender') {
|
|
return transaction.userGradidoID
|
|
} else if (type === 'recipient') {
|
|
if (!transaction.linkedUserGradidoID) {
|
|
throw new LogError('missing linked user gradido id')
|
|
}
|
|
return transaction.linkedUserGradidoID
|
|
}
|
|
break
|
|
case TransactionTypeId.RECEIVE:
|
|
if (type === 'sender') {
|
|
if (!transaction.linkedUserGradidoID) {
|
|
throw new LogError('missing linked user gradido id')
|
|
}
|
|
return transaction.linkedUserGradidoID
|
|
} else if (type === 'recipient') {
|
|
return transaction.userGradidoID
|
|
}
|
|
}
|
|
throw new LogError('unhandled case')
|
|
}
|
|
|
|
protected async getCorrectUserIdentifier(
|
|
transaction: DbTransaction,
|
|
senderCommunityUuid: string,
|
|
type: 'sender' | 'recipient',
|
|
recipientCommunityUuid?: string,
|
|
): Promise<UserIdentifier> {
|
|
// sender and receiver user on creation transaction
|
|
// sender user on send transaction (SEND and RECEIVE)
|
|
if (type === 'sender' || transaction.typeId === TransactionTypeId.CREATION) {
|
|
return {
|
|
uuid: await this.getCorrectUserUUID(transaction, type),
|
|
communityUuid: senderCommunityUuid,
|
|
}
|
|
}
|
|
// recipient user on SEND and RECEIVE transactions
|
|
return {
|
|
uuid: await this.getCorrectUserUUID(transaction, type),
|
|
communityUuid: recipientCommunityUuid ?? senderCommunityUuid,
|
|
}
|
|
}
|
|
|
|
/**
|
|
* transmit transaction via dlt-connector to iota
|
|
* and update dltTransactionId of transaction in db with iota message id
|
|
*/
|
|
public async transmitTransaction(
|
|
transaction: DbTransaction,
|
|
senderCommunityUuid: string,
|
|
recipientCommunityUuid?: string,
|
|
): Promise<boolean> {
|
|
const typeString = getTransactionTypeString(transaction.typeId)
|
|
// no negative values in dlt connector, gradido concept don't use negative values so the code don't use it too
|
|
const amountString = transaction.amount.abs().toString()
|
|
const params = {
|
|
input: {
|
|
senderUser: await this.getCorrectUserIdentifier(
|
|
transaction,
|
|
senderCommunityUuid,
|
|
'sender',
|
|
recipientCommunityUuid,
|
|
),
|
|
recipientUser: await this.getCorrectUserIdentifier(
|
|
transaction,
|
|
senderCommunityUuid,
|
|
'recipient',
|
|
recipientCommunityUuid,
|
|
),
|
|
amount: amountString,
|
|
type: typeString,
|
|
createdAt: transaction.balanceDate.toISOString(),
|
|
backendTransactionId: transaction.id,
|
|
targetDate: transaction.creationDate?.toISOString(),
|
|
},
|
|
}
|
|
try {
|
|
// TODO: add account nr for user after they have also more than one account in backend
|
|
logger.debug('transmit transaction to dlt connector', params)
|
|
const {
|
|
data: {
|
|
sendTransaction: { error, succeed },
|
|
},
|
|
} = await this.client.rawRequest<{ sendTransaction: TransactionResult }>(
|
|
sendTransaction,
|
|
params,
|
|
)
|
|
if (error) {
|
|
throw new Error(error.message)
|
|
}
|
|
return succeed
|
|
} catch (e) {
|
|
throw new LogError('Error send sending transaction to dlt-connector: ', e)
|
|
}
|
|
}
|
|
}
|