From 277d0a4b38771967849b8aa1f3e20d1a44503e05 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Fri, 11 Mar 2022 12:41:13 +0100 Subject: [PATCH 1/5] set rights --- backend/src/auth/RIGHTS.ts | 1 + backend/src/auth/ROLES.ts | 1 + backend/src/graphql/resolver/TransactionLinkResolver.ts | 6 ++++++ 3 files changed, 8 insertions(+) diff --git a/backend/src/auth/RIGHTS.ts b/backend/src/auth/RIGHTS.ts index 4b9036eed..da6bc90cc 100644 --- a/backend/src/auth/RIGHTS.ts +++ b/backend/src/auth/RIGHTS.ts @@ -21,6 +21,7 @@ export enum RIGHTS { CREATE_TRANSACTION_LINK = 'CREATE_TRANSACTION_LINK', DELETE_TRANSACTION_LINK = 'DELETE_TRANSACTION_LINK', QUERY_TRANSACTION_LINK = 'QUERY_TRANSACTION_LINK', + REDEEM_TRANSACTION_LINK = 'REDEEM_TRANSACTION_LINK', // Admin SEARCH_USERS = 'SEARCH_USERS', CREATE_PENDING_CREATION = 'CREATE_PENDING_CREATION', diff --git a/backend/src/auth/ROLES.ts b/backend/src/auth/ROLES.ts index 2a86b5bab..8a1c61bff 100644 --- a/backend/src/auth/ROLES.ts +++ b/backend/src/auth/ROLES.ts @@ -20,6 +20,7 @@ export const ROLE_USER = new Role('user', [ RIGHTS.HAS_ELOPAGE, RIGHTS.CREATE_TRANSACTION_LINK, RIGHTS.DELETE_TRANSACTION_LINK, + RIGHTS.REDEEM_TRANSACTION_LINK, ]) export const ROLE_ADMIN = new Role('admin', Object.values(RIGHTS)) // all rights diff --git a/backend/src/graphql/resolver/TransactionLinkResolver.ts b/backend/src/graphql/resolver/TransactionLinkResolver.ts index 071444de9..e78fccf10 100644 --- a/backend/src/graphql/resolver/TransactionLinkResolver.ts +++ b/backend/src/graphql/resolver/TransactionLinkResolver.ts @@ -122,4 +122,10 @@ export class TransactionLinkResolver { } return new TransactionLink(transactionLink, new User(user), userRedeem) } + + /* + @Authorized([RIGHTS.REDEEM_TRANSACTION_LINK]) + @Mutation(() => Boolean) + async redeemTransactionLink(@Arg('id') id: number, @Ctx() context: any) + */ } From 81cd858eb4a8177e807f3b96a771d4934bd37f9a Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Fri, 11 Mar 2022 13:47:04 +0100 Subject: [PATCH 2/5] call transaction resolver to redeem transactoin link. Optional senderId parameter for sendCoins to redeem links --- .../src/graphql/arg/TransactionSendArgs.ts | 5 ++- .../resolver/TransactionLinkResolver.ts | 41 +++++++++++++++++-- .../graphql/resolver/TransactionResolver.ts | 6 ++- 3 files changed, 46 insertions(+), 6 deletions(-) diff --git a/backend/src/graphql/arg/TransactionSendArgs.ts b/backend/src/graphql/arg/TransactionSendArgs.ts index e75921383..d85f6d14e 100644 --- a/backend/src/graphql/arg/TransactionSendArgs.ts +++ b/backend/src/graphql/arg/TransactionSendArgs.ts @@ -1,4 +1,4 @@ -import { ArgsType, Field } from 'type-graphql' +import { ArgsType, Field, Int } from 'type-graphql' import Decimal from 'decimal.js-light' @ArgsType() @@ -11,4 +11,7 @@ export default class TransactionSendArgs { @Field(() => String) memo: string + + @Field(() => Int, { nullable: true }) + senderId?: number } diff --git a/backend/src/graphql/resolver/TransactionLinkResolver.ts b/backend/src/graphql/resolver/TransactionLinkResolver.ts index e78fccf10..c19a42ca5 100644 --- a/backend/src/graphql/resolver/TransactionLinkResolver.ts +++ b/backend/src/graphql/resolver/TransactionLinkResolver.ts @@ -13,6 +13,7 @@ import { RIGHTS } from '@/auth/RIGHTS' import { randomBytes } from 'crypto' import { User } from '@model/User' import { calculateDecay } from '@/util/decay' +import { TransactionResolver } from './TransactionResolver' // TODO: do not export, test it inside the resolver export const transactionLinkCode = (date: Date): string => { @@ -123,9 +124,43 @@ export class TransactionLinkResolver { return new TransactionLink(transactionLink, new User(user), userRedeem) } - /* @Authorized([RIGHTS.REDEEM_TRANSACTION_LINK]) @Mutation(() => Boolean) - async redeemTransactionLink(@Arg('id') id: number, @Ctx() context: any) - */ + async redeemTransactionLink(@Arg('id') id: number, @Ctx() context: any): Promise { + const userRepository = getCustomRepository(UserRepository) + const user = await userRepository.findByPubkeyHex(context.pubKey) + const transactionLink = await dbTransactionLink.findOneOrFail({ id }) + + const now = new Date() + + if (user.id === transactionLink.userId) { + throw new Error('Cannot redeem own transaction link.') + } + + if (transactionLink.validUntil.getTime() < now.getTime()) { + throw new Error('Transaction Link is not valid anymore.') + } + + if (transactionLink.redeemedBy) { + throw new Error('Transaction Link already redeemed.') + } + + const transactionResolver = new TransactionResolver() + transactionResolver.sendCoins( + { + email: user.email, + amount: transactionLink.amount, + memo: transactionLink.memo, + senderId: transactionLink.userId, + }, + context, + ) + transactionLink.redeemedAt = now + transactionLink.redeemedBy = user.id + transactionLink.save().catch(() => { + throw new Error('Could not update transaction link.') + }) + + return true + } } diff --git a/backend/src/graphql/resolver/TransactionResolver.ts b/backend/src/graphql/resolver/TransactionResolver.ts index 1d1a00c39..ac7d03f17 100644 --- a/backend/src/graphql/resolver/TransactionResolver.ts +++ b/backend/src/graphql/resolver/TransactionResolver.ts @@ -145,12 +145,14 @@ export class TransactionResolver { @Authorized([RIGHTS.SEND_COINS]) @Mutation(() => String) async sendCoins( - @Args() { email, amount, memo }: TransactionSendArgs, + @Args() { email, amount, memo, senderId = 0 }: TransactionSendArgs, @Ctx() context: any, ): Promise { // TODO this is subject to replay attacks const userRepository = getCustomRepository(UserRepository) - const senderUser = await userRepository.findByPubkeyHex(context.pubKey) + const senderUser = senderId + ? await dbUser.findOneOrFail({ id: senderId }) + : await userRepository.findByPubkeyHex(context.pubKey) if (senderUser.pubKey.length !== 32) { throw new Error('invalid sender public key') } From 13fe4300d29f5887e5ca7e34d1c45f47020d03bb Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Fri, 11 Mar 2022 15:19:01 +0100 Subject: [PATCH 3/5] extract transaction process in function --- .../src/graphql/arg/TransactionSendArgs.ts | 5 +- .../resolver/TransactionLinkResolver.ts | 18 +-- .../graphql/resolver/TransactionResolver.ts | 146 ++++++++++-------- 3 files changed, 87 insertions(+), 82 deletions(-) diff --git a/backend/src/graphql/arg/TransactionSendArgs.ts b/backend/src/graphql/arg/TransactionSendArgs.ts index d85f6d14e..e75921383 100644 --- a/backend/src/graphql/arg/TransactionSendArgs.ts +++ b/backend/src/graphql/arg/TransactionSendArgs.ts @@ -1,4 +1,4 @@ -import { ArgsType, Field, Int } from 'type-graphql' +import { ArgsType, Field } from 'type-graphql' import Decimal from 'decimal.js-light' @ArgsType() @@ -11,7 +11,4 @@ export default class TransactionSendArgs { @Field(() => String) memo: string - - @Field(() => Int, { nullable: true }) - senderId?: number } diff --git a/backend/src/graphql/resolver/TransactionLinkResolver.ts b/backend/src/graphql/resolver/TransactionLinkResolver.ts index c19a42ca5..3eb64b2b0 100644 --- a/backend/src/graphql/resolver/TransactionLinkResolver.ts +++ b/backend/src/graphql/resolver/TransactionLinkResolver.ts @@ -13,7 +13,8 @@ import { RIGHTS } from '@/auth/RIGHTS' import { randomBytes } from 'crypto' import { User } from '@model/User' import { calculateDecay } from '@/util/decay' -import { TransactionResolver } from './TransactionResolver' +import { executeTransaction } from './TransactionResolver' +import { User as dbUser } from '@entity/User' // TODO: do not export, test it inside the resolver export const transactionLinkCode = (date: Date): string => { @@ -130,10 +131,11 @@ export class TransactionLinkResolver { const userRepository = getCustomRepository(UserRepository) const user = await userRepository.findByPubkeyHex(context.pubKey) const transactionLink = await dbTransactionLink.findOneOrFail({ id }) + const linkedUser = await dbUser.findOneOrFail({ id: transactionLink.userId }) const now = new Date() - if (user.id === transactionLink.userId) { + if (user.id === linkedUser.id) { throw new Error('Cannot redeem own transaction link.') } @@ -145,16 +147,8 @@ export class TransactionLinkResolver { throw new Error('Transaction Link already redeemed.') } - const transactionResolver = new TransactionResolver() - transactionResolver.sendCoins( - { - email: user.email, - amount: transactionLink.amount, - memo: transactionLink.memo, - senderId: transactionLink.userId, - }, - context, - ) + await executeTransaction(transactionLink.amount, transactionLink.memo, linkedUser, user) + transactionLink.redeemedAt = now transactionLink.redeemedBy = user.id transactionLink.save().catch(() => { diff --git a/backend/src/graphql/resolver/TransactionResolver.ts b/backend/src/graphql/resolver/TransactionResolver.ts index ac7d03f17..3008fc8d5 100644 --- a/backend/src/graphql/resolver/TransactionResolver.ts +++ b/backend/src/graphql/resolver/TransactionResolver.ts @@ -34,6 +34,83 @@ import { virtualDecayTransaction } from '@/util/virtualDecayTransaction' import Decimal from 'decimal.js-light' import { calculateDecay } from '@/util/decay' +export const executeTransaction = async ( + amount: Decimal, + memo: string, + sender: dbUser, + recipient: dbUser, +): Promise => { + if (sender.id === recipient.id) { + throw new Error('Sender and Recipient are the same.') + } + + // validate amount + const receivedCallDate = new Date() + const sendBalance = await calculateBalance(sender.id, amount.mul(-1), receivedCallDate) + if (!sendBalance) { + throw new Error("user hasn't enough GDD or amount is < 0") + } + + const queryRunner = getConnection().createQueryRunner() + await queryRunner.connect() + await queryRunner.startTransaction('READ UNCOMMITTED') + try { + // transaction + const transactionSend = new dbTransaction() + transactionSend.typeId = TransactionTypeId.SEND + transactionSend.memo = memo + transactionSend.userId = sender.id + transactionSend.linkedUserId = recipient.id + transactionSend.amount = amount.mul(-1) + transactionSend.balance = sendBalance.balance + transactionSend.balanceDate = receivedCallDate + transactionSend.decay = sendBalance.decay.decay + transactionSend.decayStart = sendBalance.decay.start + transactionSend.previous = sendBalance.lastTransactionId + await queryRunner.manager.insert(dbTransaction, transactionSend) + + const transactionReceive = new dbTransaction() + transactionReceive.typeId = TransactionTypeId.RECEIVE + transactionReceive.memo = memo + transactionReceive.userId = recipient.id + transactionReceive.linkedUserId = sender.id + transactionReceive.amount = amount + const receiveBalance = await calculateBalance(recipient.id, amount, receivedCallDate) + transactionReceive.balance = receiveBalance ? receiveBalance.balance : amount + transactionReceive.balanceDate = receivedCallDate + transactionReceive.decay = receiveBalance ? receiveBalance.decay.decay : new Decimal(0) + transactionReceive.decayStart = receiveBalance ? receiveBalance.decay.start : null + transactionReceive.previous = receiveBalance ? receiveBalance.lastTransactionId : null + transactionReceive.linkedTransactionId = transactionSend.id + await queryRunner.manager.insert(dbTransaction, transactionReceive) + + // Save linked transaction id for send + transactionSend.linkedTransactionId = transactionReceive.id + await queryRunner.manager.update(dbTransaction, { id: transactionSend.id }, transactionSend) + + await queryRunner.commitTransaction() + } catch (e) { + await queryRunner.rollbackTransaction() + throw new Error(`Transaction was not successful: ${e}`) + } finally { + await queryRunner.release() + } + + // send notification email + // TODO: translate + await sendTransactionReceivedEmail({ + senderFirstName: sender.firstName, + senderLastName: sender.lastName, + recipientFirstName: recipient.firstName, + recipientLastName: recipient.lastName, + email: recipient.email, + amount, + memo, + }) + + return true +} + @Resolver() export class TransactionResolver { @Authorized([RIGHTS.TRANSACTION_LIST]) @@ -145,23 +222,15 @@ export class TransactionResolver { @Authorized([RIGHTS.SEND_COINS]) @Mutation(() => String) async sendCoins( - @Args() { email, amount, memo, senderId = 0 }: TransactionSendArgs, + @Args() { email, amount, memo }: TransactionSendArgs, @Ctx() context: any, ): Promise { // TODO this is subject to replay attacks const userRepository = getCustomRepository(UserRepository) - const senderUser = senderId - ? await dbUser.findOneOrFail({ id: senderId }) - : await userRepository.findByPubkeyHex(context.pubKey) + const senderUser = await userRepository.findByPubkeyHex(context.pubKey) if (senderUser.pubKey.length !== 32) { throw new Error('invalid sender public key') } - // validate amount - const receivedCallDate = new Date() - const sendBalance = await calculateBalance(senderUser.id, amount.mul(-1), receivedCallDate) - if (!sendBalance) { - throw new Error("user hasn't enough GDD or amount is < 0") - } // validate recipient user const recipientUser = await dbUser.findOne({ email: email }, { withDeleted: true }) @@ -175,62 +244,7 @@ export class TransactionResolver { throw new Error('invalid recipient public key') } - const queryRunner = getConnection().createQueryRunner() - await queryRunner.connect() - await queryRunner.startTransaction('READ UNCOMMITTED') - try { - // transaction - const transactionSend = new dbTransaction() - transactionSend.typeId = TransactionTypeId.SEND - transactionSend.memo = memo - transactionSend.userId = senderUser.id - transactionSend.linkedUserId = recipientUser.id - transactionSend.amount = amount.mul(-1) - transactionSend.balance = sendBalance.balance - transactionSend.balanceDate = receivedCallDate - transactionSend.decay = sendBalance.decay.decay - transactionSend.decayStart = sendBalance.decay.start - transactionSend.previous = sendBalance.lastTransactionId - await queryRunner.manager.insert(dbTransaction, transactionSend) - - const transactionReceive = new dbTransaction() - transactionReceive.typeId = TransactionTypeId.RECEIVE - transactionReceive.memo = memo - transactionReceive.userId = recipientUser.id - transactionReceive.linkedUserId = senderUser.id - transactionReceive.amount = amount - const receiveBalance = await calculateBalance(recipientUser.id, amount, receivedCallDate) - transactionReceive.balance = receiveBalance ? receiveBalance.balance : amount - transactionReceive.balanceDate = receivedCallDate - transactionReceive.decay = receiveBalance ? receiveBalance.decay.decay : new Decimal(0) - transactionReceive.decayStart = receiveBalance ? receiveBalance.decay.start : null - transactionReceive.previous = receiveBalance ? receiveBalance.lastTransactionId : null - transactionReceive.linkedTransactionId = transactionSend.id - await queryRunner.manager.insert(dbTransaction, transactionReceive) - - // Save linked transaction id for send - transactionSend.linkedTransactionId = transactionReceive.id - await queryRunner.manager.update(dbTransaction, { id: transactionSend.id }, transactionSend) - - await queryRunner.commitTransaction() - } catch (e) { - await queryRunner.rollbackTransaction() - throw new Error(`Transaction was not successful: ${e}`) - } finally { - await queryRunner.release() - } - - // send notification email - // TODO: translate - await sendTransactionReceivedEmail({ - senderFirstName: senderUser.firstName, - senderLastName: senderUser.lastName, - recipientFirstName: recipientUser.firstName, - recipientLastName: recipientUser.lastName, - email: recipientUser.email, - amount, - memo, - }) + await executeTransaction(amount, memo, senderUser, recipientUser) return true } From 13c61984de19f099e5bfc569d37015c0ae0e393c Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Fri, 11 Mar 2022 15:27:02 +0100 Subject: [PATCH 4/5] liniting --- backend/src/graphql/resolver/TransactionLinkResolver.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/src/graphql/resolver/TransactionLinkResolver.ts b/backend/src/graphql/resolver/TransactionLinkResolver.ts index 048c804e4..7aeb72fc1 100644 --- a/backend/src/graphql/resolver/TransactionLinkResolver.ts +++ b/backend/src/graphql/resolver/TransactionLinkResolver.ts @@ -15,7 +15,6 @@ import { randomBytes } from 'crypto' import { User } from '@model/User' import { calculateDecay } from '@/util/decay' import { executeTransaction } from './TransactionResolver' -import { User as dbUser } from '@entity/User' import { Order } from '@enum/Order' // TODO: do not export, test it inside the resolver From 826ad7019c2196db02eef43b5d0e5c1146b22a01 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Fri, 11 Mar 2022 15:33:36 +0100 Subject: [PATCH 5/5] add TODO --- backend/src/graphql/resolver/TransactionLinkResolver.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/src/graphql/resolver/TransactionLinkResolver.ts b/backend/src/graphql/resolver/TransactionLinkResolver.ts index 7aeb72fc1..835b84cbf 100644 --- a/backend/src/graphql/resolver/TransactionLinkResolver.ts +++ b/backend/src/graphql/resolver/TransactionLinkResolver.ts @@ -158,6 +158,7 @@ export class TransactionLinkResolver { await executeTransaction(transactionLink.amount, transactionLink.memo, linkedUser, user) + // TODO: Rollback transaction when updating links fails transactionLink.redeemedAt = now transactionLink.redeemedBy = user.id transactionLink.save().catch(() => {