mirror of
https://github.com/IT4Change/gradido.git
synced 2026-02-06 09:56:05 +00:00
456 lines
19 KiB
TypeScript
456 lines
19 KiB
TypeScript
import {
|
|
CommunityLoggingView,
|
|
countOpenPendingTransactions,
|
|
Community as DbCommunity,
|
|
FederatedCommunity as DbFederatedCommunity,
|
|
PendingTransaction as DbPendingTransaction,
|
|
User as dbUser,
|
|
findTransactionLinkByCode,
|
|
findUserByIdentifier,
|
|
getCommunityByUuid,
|
|
PendingTransactionLoggingView,
|
|
UserLoggingView
|
|
} from 'database'
|
|
import { Decimal } from 'decimal.js-light'
|
|
|
|
import { CONFIG as CONFIG_CORE } from '../../config'
|
|
import { LOG4JS_BASE_CATEGORY_NAME } from '../../config/const'
|
|
|
|
import { SendCoinsClient as V1_0_SendCoinsClient } from '../../federation/client/1_0/SendCoinsClient'
|
|
import { SendCoinsResult } from '../../federation/client/1_0/model/SendCoinsResult'
|
|
import { SendCoinsClientFactory } from '../../federation/client/SendCoinsClientFactory'
|
|
import { TransactionTypeId } from '../../graphql/enum/TransactionTypeId'
|
|
import { encryptAndSign, PendingTransactionState, SendCoinsJwtPayloadType, SendCoinsResponseJwtPayloadType, verifyAndDecrypt } from 'shared'
|
|
// import { LogError } from '@server/LogError'
|
|
import { calculateSenderBalance } from '../../util/calculateSenderBalance'
|
|
import { fullName } from '../../util/utilities'
|
|
import { getLogger } from 'log4js'
|
|
|
|
import { SendCoinsResultLoggingView } from '../../federation/client/1_0/logging/SendCoinsResultLogging.view'
|
|
import { EncryptedTransferArgs } from '../../graphql/model/EncryptedTransferArgs'
|
|
import { randombytes_random } from 'sodium-native'
|
|
import { settlePendingSenderTransaction } from './settlePendingSenderTransaction'
|
|
import { storeForeignUser } from './storeForeignUser'
|
|
|
|
const createLogger = (method: string) => getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.resolver.util.processXComSendCoins.${method}`)
|
|
|
|
export async function processXComCompleteTransaction(
|
|
senderCommunityUuid: string,
|
|
senderGradidoId: string,
|
|
recipientCommunityUuid: string,
|
|
recipientGradidoId: string,
|
|
amount: string,
|
|
memo: string,
|
|
code?: string,
|
|
recipientFirstName?: string,
|
|
recipientAlias?: string,
|
|
creationDate?: Date,
|
|
): Promise<boolean> {
|
|
const methodLogger = createLogger(`processXComCompleteTransaction`)
|
|
// processing a x-community sendCoins
|
|
methodLogger.info('processing a x-community transaction...')
|
|
if (!CONFIG_CORE.FEDERATION_XCOM_SENDCOINS_ENABLED) {
|
|
const errmsg = 'X-Community sendCoins disabled per configuration!'
|
|
methodLogger.error(errmsg)
|
|
throw new Error(errmsg)
|
|
}
|
|
const senderCom = await getCommunityByUuid(senderCommunityUuid)
|
|
methodLogger.debug('sender community: ', senderCom?.id)
|
|
if (senderCom === null) {
|
|
const errmsg = `no sender community found for identifier: ${senderCommunityUuid}`
|
|
methodLogger.error(errmsg)
|
|
throw new Error(errmsg)
|
|
}
|
|
const senderUser = await findUserByIdentifier(senderGradidoId, senderCommunityUuid)
|
|
if (senderUser === null) {
|
|
const errmsg = `no sender user found for identifier: ${senderCommunityUuid}:${senderGradidoId}`
|
|
methodLogger.error(errmsg)
|
|
throw new Error(errmsg)
|
|
}
|
|
const recipientCom = await getCommunityByUuid(recipientCommunityUuid)
|
|
methodLogger.debug('recipient community: ', recipientCom?.id)
|
|
if (recipientCom === null) {
|
|
const errmsg = `no recipient community found for identifier: ${recipientCommunityUuid}`
|
|
methodLogger.error(errmsg)
|
|
throw new Error(errmsg)
|
|
}
|
|
if (recipientCom !== null && recipientCom.authenticatedAt === null) {
|
|
const errmsg = 'recipient community is connected, but still not authenticated yet!'
|
|
methodLogger.error(errmsg)
|
|
throw new Error(errmsg)
|
|
}
|
|
if(code !== undefined) {
|
|
try {
|
|
const dbTransactionLink = await findTransactionLinkByCode(code)
|
|
if (dbTransactionLink && dbTransactionLink.validUntil < new Date()) {
|
|
const errmsg = `TransactionLink ${code} is expired!`
|
|
methodLogger.error(errmsg)
|
|
throw new Error(errmsg)
|
|
}
|
|
} catch (_err) {
|
|
const errmsg = `TransactionLink ${code} not found any more!`
|
|
methodLogger.error(errmsg)
|
|
throw new Error(errmsg)
|
|
}
|
|
}
|
|
if(creationDate === undefined) {
|
|
creationDate = new Date()
|
|
}
|
|
let pendingResult: SendCoinsResponseJwtPayloadType | null = null
|
|
let committingResult: SendCoinsResult
|
|
|
|
try {
|
|
pendingResult = await processXComPendingSendCoins(
|
|
recipientCom,
|
|
senderCom,
|
|
creationDate,
|
|
new Decimal(amount),
|
|
memo,
|
|
senderUser,
|
|
recipientGradidoId,
|
|
)
|
|
methodLogger.debug('processXComPendingSendCoins result: ', pendingResult)
|
|
if (pendingResult && pendingResult.vote && pendingResult.recipGradidoID) {
|
|
methodLogger.debug('vor processXComCommittingSendCoins... ')
|
|
committingResult = await processXComCommittingSendCoins(
|
|
recipientCom,
|
|
senderCom,
|
|
creationDate,
|
|
new Decimal(amount),
|
|
memo,
|
|
senderUser,
|
|
pendingResult,
|
|
)
|
|
methodLogger.debug('processXComCommittingSendCoins result: ', committingResult)
|
|
if (!committingResult.vote) {
|
|
methodLogger.fatal('FATAL ERROR: on processXComCommittingSendCoins for', committingResult)
|
|
throw new Error(
|
|
'FATAL ERROR: on processXComCommittingSendCoins with ' +
|
|
recipientCom.communityUuid +
|
|
recipientGradidoId +
|
|
amount.toString() +
|
|
memo,
|
|
)
|
|
}
|
|
// after successful x-com-tx store the recipient as foreign user
|
|
methodLogger.debug('store recipient as foreign user...')
|
|
if (await storeForeignUser(recipientCom, committingResult)) {
|
|
methodLogger.info(
|
|
'X-Com: new foreign user inserted successfully...',
|
|
recipientCom.communityUuid,
|
|
committingResult.recipGradidoID,
|
|
)
|
|
}
|
|
}
|
|
} catch (err) {
|
|
const errmsg = `ERROR: on processXComCommittingSendCoins with ` +
|
|
recipientCommunityUuid +
|
|
recipientGradidoId +
|
|
amount.toString() +
|
|
memo +
|
|
err
|
|
methodLogger.error(errmsg)
|
|
throw new Error(errmsg)
|
|
}
|
|
return true
|
|
}
|
|
|
|
export async function processXComPendingSendCoins(
|
|
receiverCom: DbCommunity,
|
|
senderCom: DbCommunity,
|
|
creationDate: Date,
|
|
amount: Decimal,
|
|
memo: string,
|
|
sender: dbUser,
|
|
recipientIdentifier: string,
|
|
): Promise<SendCoinsResponseJwtPayloadType | null> {
|
|
let voteResult: SendCoinsResponseJwtPayloadType
|
|
const methodLogger = createLogger(`processXComPendingSendCoins`)
|
|
try {
|
|
// even if debug is not enabled, attributes are processed so we skip the entire call for performance reasons
|
|
if(methodLogger.isDebugEnabled()) {
|
|
methodLogger.debug(
|
|
'XCom: processXComPendingSendCoins...', {
|
|
receiverCom: new CommunityLoggingView(receiverCom),
|
|
senderCom: new CommunityLoggingView(senderCom),
|
|
amount: amount.toString(),
|
|
memo: memo.substring(0, 5),
|
|
sender: new UserLoggingView(sender),
|
|
recipientIdentifier
|
|
}
|
|
)
|
|
}
|
|
if (await countOpenPendingTransactions([sender.gradidoID, recipientIdentifier]) > 0) {
|
|
const errmsg = `There exist still ongoing 'Pending-Transactions' for the involved users on sender-side!`
|
|
methodLogger.error(errmsg)
|
|
throw new Error(errmsg)
|
|
}
|
|
const handshakeID = randombytes_random().toString()
|
|
methodLogger.addContext('handshakeID', handshakeID)
|
|
// first calculate the sender balance and check if the transaction is allowed
|
|
const senderBalance = await calculateSenderBalance(sender.id, amount.mul(-1), creationDate)
|
|
if (!senderBalance) {
|
|
const errmsg = `User has not enough GDD or amount is < 0`
|
|
methodLogger.error(errmsg)
|
|
throw new Error(errmsg)
|
|
}
|
|
if(methodLogger.isDebugEnabled()) {
|
|
methodLogger.debug(`calculated senderBalance = ${JSON.stringify(senderBalance, null, 2)}`)
|
|
}
|
|
|
|
const receiverFCom = await DbFederatedCommunity.findOneOrFail({
|
|
where: {
|
|
publicKey: Buffer.from(receiverCom.publicKey),
|
|
apiVersion: CONFIG_CORE.FEDERATION_BACKEND_SEND_ON_API,
|
|
},
|
|
})
|
|
const client = SendCoinsClientFactory.getInstance(receiverFCom)
|
|
|
|
if (client instanceof V1_0_SendCoinsClient) {
|
|
const payload = new SendCoinsJwtPayloadType(handshakeID,
|
|
receiverCom.communityUuid!,
|
|
recipientIdentifier,
|
|
creationDate.toISOString(),
|
|
amount,
|
|
memo,
|
|
senderCom.communityUuid!,
|
|
sender.gradidoID,
|
|
fullName(sender.firstName, sender.lastName),
|
|
sender.alias
|
|
)
|
|
if(methodLogger.isDebugEnabled()) {
|
|
methodLogger.debug(`ready for voteForSendCoins with payload=${payload}`)
|
|
}
|
|
const jws = await encryptAndSign(payload, senderCom.privateJwtKey!, receiverCom.publicJwtKey!)
|
|
if(methodLogger.isDebugEnabled()) {
|
|
methodLogger.debug('jws', jws)
|
|
}
|
|
// prepare the args for the client invocation
|
|
const args = new EncryptedTransferArgs()
|
|
args.publicKey = senderCom.publicKey.toString('hex')
|
|
args.jwt = jws
|
|
args.handshakeID = handshakeID
|
|
if(methodLogger.isDebugEnabled()) {
|
|
methodLogger.debug('before client.voteForSendCoins() args:', args)
|
|
}
|
|
|
|
const responseJwt = await client.voteForSendCoins(args)
|
|
if(methodLogger.isDebugEnabled()) {
|
|
methodLogger.debug(`response of voteForSendCoins():`, responseJwt)
|
|
}
|
|
if (responseJwt !== null) {
|
|
voteResult = await verifyAndDecrypt(handshakeID, responseJwt, senderCom.privateJwtKey!, receiverCom.publicJwtKey!) as SendCoinsResponseJwtPayloadType
|
|
if(methodLogger.isDebugEnabled()) {
|
|
methodLogger.debug(`received payload from voteForSendCoins():`, voteResult)
|
|
}
|
|
if (voteResult && voteResult.tokentype !== SendCoinsResponseJwtPayloadType.SEND_COINS_RESPONSE_TYPE) {
|
|
const errmsg = `Invalid tokentype in voteForSendCoins-response of community with publicKey` + receiverCom.publicKey
|
|
methodLogger.error(errmsg)
|
|
throw new Error('Error in X-Com-TX protocol...')
|
|
}
|
|
if (voteResult && voteResult.vote) {
|
|
methodLogger.debug('prepare pendingTransaction for sender...')
|
|
// writing the pending transaction on receiver-side was successfull, so now write the sender side
|
|
try {
|
|
const pendingTx = DbPendingTransaction.create()
|
|
pendingTx.amount = amount.mul(-1)
|
|
pendingTx.balance = senderBalance.balance
|
|
pendingTx.balanceDate = creationDate
|
|
pendingTx.decay = senderBalance ? senderBalance.decay.decay : new Decimal(0)
|
|
pendingTx.decayStart = senderBalance ? senderBalance.decay.start : null
|
|
if (receiverCom.communityUuid) {
|
|
pendingTx.linkedUserCommunityUuid = receiverCom.communityUuid
|
|
}
|
|
if (voteResult.recipGradidoID) {
|
|
pendingTx.linkedUserGradidoID = voteResult.recipGradidoID
|
|
}
|
|
if (voteResult.recipFirstName && voteResult.recipLastName) {
|
|
pendingTx.linkedUserName = fullName(voteResult.recipFirstName, voteResult.recipLastName)
|
|
}
|
|
pendingTx.memo = memo
|
|
pendingTx.previous = senderBalance ? senderBalance.lastTransactionId : null
|
|
pendingTx.state = PendingTransactionState.NEW
|
|
pendingTx.typeId = TransactionTypeId.SEND
|
|
if (senderCom.communityUuid) {
|
|
pendingTx.userCommunityUuid = senderCom.communityUuid
|
|
}
|
|
pendingTx.userId = sender.id
|
|
pendingTx.userGradidoID = sender.gradidoID
|
|
pendingTx.userName = fullName(sender.firstName, sender.lastName)
|
|
if(methodLogger.isDebugEnabled()) {
|
|
methodLogger.debug(`initialized sender pendingTX=${new PendingTransactionLoggingView(pendingTx)}`)
|
|
}
|
|
|
|
await DbPendingTransaction.insert(pendingTx)
|
|
methodLogger.debug('sender pendingTx successfully inserted...')
|
|
} catch (err) {
|
|
methodLogger.error(`Error in writing sender pending transaction: ${JSON.stringify(err, null, 2)}`)
|
|
// revert the existing pending transaction on receiver side
|
|
let revertCount = 0
|
|
methodLogger.debug('first try to revertSendCoins of receiver')
|
|
do {
|
|
if (await client.revertSendCoins(args)) {
|
|
methodLogger.debug(`revertSendCoins()-1_0... successfull after revertCount=${revertCount}`)
|
|
// treat revertingSendCoins as an error of the whole sendCoins-process
|
|
const errmsg = `Error in writing sender pending transaction: ${JSON.stringify(err, null, 2)}`
|
|
methodLogger.error(errmsg)
|
|
throw new Error(errmsg)
|
|
}
|
|
} while (CONFIG_CORE.FEDERATION_XCOM_MAXREPEAT_REVERTSENDCOINS > revertCount++)
|
|
const errmsg = `Error in reverting receiver pending transaction even after revertCount=${revertCount}` + JSON.stringify(err, null, 2)
|
|
methodLogger.error(errmsg)
|
|
throw new Error(errmsg)
|
|
}
|
|
methodLogger.debug('voteForSendCoins()-1_0... successfull')
|
|
return voteResult
|
|
} else {
|
|
methodLogger.error(`break with error on writing pendingTransaction for recipient... ${voteResult}`)
|
|
}
|
|
} else {
|
|
methodLogger.error(`break with no response from voteForSendCoins()-1_0...`)
|
|
}
|
|
}
|
|
} catch (err: any) {
|
|
const errmsg = `Error: ${err.message}` + JSON.stringify(err, null, 2)
|
|
methodLogger.error(errmsg)
|
|
throw new Error(errmsg)
|
|
}
|
|
return null
|
|
}
|
|
|
|
export async function processXComCommittingSendCoins(
|
|
receiverCom: DbCommunity,
|
|
senderCom: DbCommunity,
|
|
creationDate: Date,
|
|
amount: Decimal,
|
|
memo: string,
|
|
sender: dbUser,
|
|
recipient: SendCoinsResult,
|
|
): Promise<SendCoinsResult> {
|
|
const methodLogger = createLogger(`processXComCommittingSendCoins`)
|
|
const handshakeID = randombytes_random().toString()
|
|
methodLogger.addContext('handshakeID', handshakeID)
|
|
const sendCoinsResult = new SendCoinsResult()
|
|
try {
|
|
if(methodLogger.isDebugEnabled()) {
|
|
methodLogger.debug(
|
|
'XCom: processXComCommittingSendCoins...', {
|
|
receiverCom: new CommunityLoggingView(receiverCom),
|
|
senderCom: new CommunityLoggingView(senderCom),
|
|
creationDate: creationDate.toISOString(),
|
|
amount: amount.toString(),
|
|
memo: memo.substring(0, 5),
|
|
sender: new UserLoggingView(sender),
|
|
recipient: new SendCoinsResultLoggingView(recipient),
|
|
}
|
|
)
|
|
}
|
|
// first find pending Tx with given parameters
|
|
const pendingTx = await DbPendingTransaction.findOneBy({
|
|
userCommunityUuid: senderCom.communityUuid ?? 'homeCom-UUID',
|
|
userGradidoID: sender.gradidoID,
|
|
userName: fullName(sender.firstName, sender.lastName),
|
|
linkedUserCommunityUuid:
|
|
receiverCom.communityUuid ?? CONFIG_CORE.FEDERATION_XCOM_RECEIVER_COMMUNITY_UUID,
|
|
linkedUserGradidoID: recipient.recipGradidoID ? recipient.recipGradidoID : undefined,
|
|
typeId: TransactionTypeId.SEND,
|
|
state: PendingTransactionState.NEW,
|
|
balanceDate: creationDate,
|
|
memo,
|
|
})
|
|
if (pendingTx) {
|
|
if(methodLogger.isDebugEnabled()) {
|
|
methodLogger.debug(`find pending Tx for settlement: ${new PendingTransactionLoggingView(pendingTx)}`)
|
|
}
|
|
const receiverFCom = await DbFederatedCommunity.findOneOrFail({
|
|
where: {
|
|
publicKey: Buffer.from(receiverCom.publicKey),
|
|
apiVersion: CONFIG_CORE.FEDERATION_BACKEND_SEND_ON_API,
|
|
},
|
|
})
|
|
const client = SendCoinsClientFactory.getInstance(receiverFCom)
|
|
|
|
if (client instanceof V1_0_SendCoinsClient) {
|
|
const payload = new SendCoinsJwtPayloadType(
|
|
handshakeID,
|
|
pendingTx.linkedUserCommunityUuid
|
|
? pendingTx.linkedUserCommunityUuid
|
|
: CONFIG_CORE.FEDERATION_XCOM_RECEIVER_COMMUNITY_UUID,
|
|
pendingTx.linkedUserGradidoID!,
|
|
pendingTx.balanceDate.toISOString(),
|
|
pendingTx.amount.mul(-1),
|
|
pendingTx.memo,
|
|
pendingTx.userCommunityUuid,
|
|
pendingTx.userGradidoID!,
|
|
pendingTx.userName!,
|
|
sender.alias,
|
|
)
|
|
payload.recipientCommunityUuid = pendingTx.linkedUserCommunityUuid
|
|
? pendingTx.linkedUserCommunityUuid
|
|
: CONFIG_CORE.FEDERATION_XCOM_RECEIVER_COMMUNITY_UUID
|
|
if (pendingTx.linkedUserGradidoID) {
|
|
payload.recipientUserIdentifier = pendingTx.linkedUserGradidoID
|
|
}
|
|
if(methodLogger.isDebugEnabled()) {
|
|
methodLogger.debug(`ready for settleSendCoins with payload=${ JSON.stringify(payload)}`)
|
|
}
|
|
const jws = await encryptAndSign(payload, senderCom.privateJwtKey!, receiverCom.publicJwtKey!)
|
|
// prepare the args for the client invocation
|
|
const args = new EncryptedTransferArgs()
|
|
args.publicKey = senderCom.publicKey.toString('hex')
|
|
args.jwt = jws
|
|
args.handshakeID = handshakeID
|
|
const acknowledge = await client.settleSendCoins(args)
|
|
methodLogger.debug(`return from settleSendCoins: ${acknowledge}`)
|
|
if (acknowledge) {
|
|
// settle the pending transaction on receiver-side was successfull, so now settle the sender side
|
|
try {
|
|
sendCoinsResult.vote = await settlePendingSenderTransaction(
|
|
senderCom,
|
|
sender,
|
|
pendingTx,
|
|
)
|
|
if (sendCoinsResult.vote) {
|
|
if (pendingTx.linkedUserName) {
|
|
sendCoinsResult.recipFirstName = pendingTx.linkedUserName.slice(
|
|
0,
|
|
pendingTx.linkedUserName.indexOf(' '),
|
|
)
|
|
sendCoinsResult.recipLastName = pendingTx.linkedUserName.slice(
|
|
pendingTx.linkedUserName.indexOf(' '),
|
|
pendingTx.linkedUserName.length,
|
|
)
|
|
}
|
|
sendCoinsResult.recipGradidoID = pendingTx.linkedUserGradidoID
|
|
sendCoinsResult.recipAlias = recipient.recipAlias
|
|
}
|
|
} catch (err) {
|
|
methodLogger.error(`Error in writing sender pending transaction: ${JSON.stringify(err, null, 2)}`)
|
|
// revert the existing pending transaction on receiver side
|
|
let revertCount = 0
|
|
methodLogger.debug('first try to revertSettledSendCoins of receiver')
|
|
do {
|
|
if (await client.revertSettledSendCoins(args)) {
|
|
methodLogger.debug(
|
|
`revertSettledSendCoins()-1_0... successfull after revertCount=${revertCount}`,
|
|
)
|
|
// treat revertingSettledSendCoins as an error of the whole sendCoins-process
|
|
const errmsg = `Error in settle sender pending transaction: ${JSON.stringify(err, null, 2)}`
|
|
methodLogger.error(errmsg)
|
|
throw new Error(errmsg)
|
|
}
|
|
} while (CONFIG_CORE.FEDERATION_XCOM_MAXREPEAT_REVERTSENDCOINS > revertCount++)
|
|
const errmsg = `Error in reverting receiver pending transaction even after revertCount=${revertCount}`
|
|
methodLogger.error(errmsg)
|
|
throw new Error(errmsg)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} catch (err) {
|
|
methodLogger.error(`Error: ${JSON.stringify(err, null, 2)}`)
|
|
sendCoinsResult.vote = false
|
|
}
|
|
return sendCoinsResult
|
|
}
|