diff --git a/back-end/apps/api/src/transactions/dto/transaction-group.dto.ts b/back-end/apps/api/src/transactions/dto/transaction-group.dto.ts index dbff7e2c4..14f1b2f8f 100644 --- a/back-end/apps/api/src/transactions/dto/transaction-group.dto.ts +++ b/back-end/apps/api/src/transactions/dto/transaction-group.dto.ts @@ -17,6 +17,9 @@ export class TransactionGroupDto { @Expose() createdAt: Date; + @Expose() + groupValidTime: Date; + @Expose() @Type(() => TransactionGroupItemDto) groupItems: TransactionGroupItemDto[]; diff --git a/front-end/prisma/migrations/20250114114507_add_group_valid_start_to_transaction_group/migration.sql b/front-end/prisma/migrations/20250114114507_add_group_valid_start_to_transaction_group/migration.sql new file mode 100644 index 000000000..54bfbe076 --- /dev/null +++ b/front-end/prisma/migrations/20250114114507_add_group_valid_start_to_transaction_group/migration.sql @@ -0,0 +1,15 @@ +-- RedefineTables +PRAGMA defer_foreign_keys=ON; +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_TransactionGroup" ( + "id" TEXT NOT NULL PRIMARY KEY, + "description" TEXT NOT NULL, + "atomic" BOOLEAN NOT NULL, + "created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "groupValidStart" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); +INSERT INTO "new_TransactionGroup" ("atomic", "created_at", "description", "id") SELECT "atomic", "created_at", "description", "id" FROM "TransactionGroup"; +DROP TABLE "TransactionGroup"; +ALTER TABLE "new_TransactionGroup" RENAME TO "TransactionGroup"; +PRAGMA foreign_keys=ON; +PRAGMA defer_foreign_keys=OFF; diff --git a/front-end/prisma/schema.prisma b/front-end/prisma/schema.prisma index 2c262dfb0..28c0177e6 100644 --- a/front-end/prisma/schema.prisma +++ b/front-end/prisma/schema.prisma @@ -162,6 +162,7 @@ model TransactionGroup { description String atomic Boolean created_at DateTime @default(now()) + groupValidStart DateTime @default(now()) GroupItem GroupItem[] } diff --git a/front-end/src/renderer/components/Transaction/TransactionGroupProcessor.vue b/front-end/src/renderer/components/Transaction/TransactionGroupProcessor.vue index 0da959c01..62f0603d8 100644 --- a/front-end/src/renderer/components/Transaction/TransactionGroupProcessor.vue +++ b/front-end/src/renderer/components/Transaction/TransactionGroupProcessor.vue @@ -43,6 +43,7 @@ import { isLoggedInOrganization, isUserLoggedIn, getErrorMessage, + assertIsLoggedInOrganization, } from '@renderer/utils'; import AppButton from '@renderer/components/ui/AppButton.vue'; @@ -120,12 +121,7 @@ async function signAfterConfirm() { } assertUserLoggedIn(user.personal); - - if (user.selectedOrganization) { - throw new Error( - "User is in organization mode, shouldn't be able to sign before submitting to organization", - ); - } + assertIsLoggedInOrganization(user.selectedOrganization); /* Verifies the user has entered his password */ const personalPassword = getPassword(signAfterConfirm, { @@ -269,7 +265,7 @@ async function executeTransaction(transactionBytes: Uint8Array, groupItem?: Grou await deleteDraft(savedGroupItem.transaction_draft_id!); } else if (groupItem) { if (newGroupId.value === '') { - const newGroup = await addGroup('', false); + const newGroup = await addGroup('', false, transactionGroup.groupValidStart); newGroupId.value = newGroup.id; } await addGroupItem(groupItem, newGroupId.value, storedTransaction.id); @@ -288,7 +284,7 @@ async function sendSignedTransactionsToOrganization() { /* Verifies the user has entered his password */ assertUserLoggedIn(user.personal); - const personalPassword = getPassword(signAfterConfirm, { + const personalPassword = getPassword(sendSignedTransactionsToOrganization, { subHeading: 'Enter your application password to sign as a creator', }); if (passwordModalOpened(personalPassword)) return; @@ -336,6 +332,7 @@ async function sendSignedTransactionsToOrganization() { transactionGroup.description, false, transactionGroup.sequential, + transactionGroup.groupValidStart, apiGroupItems, ); diff --git a/front-end/src/renderer/pages/CreateTransactionGroup/CreateTransactionGroup.vue b/front-end/src/renderer/pages/CreateTransactionGroup/CreateTransactionGroup.vue index 95eb3d949..ec67519a2 100644 --- a/front-end/src/renderer/pages/CreateTransactionGroup/CreateTransactionGroup.vue +++ b/front-end/src/renderer/pages/CreateTransactionGroup/CreateTransactionGroup.vue @@ -30,6 +30,7 @@ import EmptyTransactions from '@renderer/components/EmptyTransactions.vue'; import TransactionSelectionModal from '@renderer/components/TransactionSelectionModal.vue'; import TransactionGroupProcessor from '@renderer/components/Transaction/TransactionGroupProcessor.vue'; import SaveTransactionGroupModal from '@renderer/components/modals/SaveTransactionGroupModal.vue'; +import RunningClockDatePicker from '@renderer/components/RunningClockDatePicker.vue'; /* Stores */ const transactionGroup = useTransactionGroupStore(); @@ -68,7 +69,11 @@ async function saveTransactionGroup() { throw new Error('Please add at least one transaction to the group'); } - await transactionGroup.saveGroup(user.personal.id, groupDescription.value); + await transactionGroup.saveGroup( + user.personal.id, + groupDescription.value, + transactionGroup.groupValidStart, + ); transactionGroup.clearGroup(); } async function handleSaveGroup() { @@ -155,6 +160,7 @@ async function handleSignSubmit() { } try { + transactionGroup.updateTransactionValidStarts(transactionGroup.groupValidStart); const ownerKeys = new Array(); for (const key of user.keyPairs) { ownerKeys.push(PublicKey.fromString(key.public_key)); @@ -292,6 +298,10 @@ async function handleOnFileChanged(e: Event) { } } +function updateGroupValidStart(newDate: Date) { + transactionGroup.groupValidStart = newDate; +} + /* Functions */ function makeTransfer(index: number) { const transfers = ( @@ -429,12 +439,24 @@ onBeforeRouteLeave(async to => {
-
- {{ - transactionGroup.groupItems.length < 2 - ? `1 Transaction` - : `${transactionGroup.groupItems.length} Transactions` - }} +
+
+ + +
+
+ {{ + transactionGroup.groupItems.length < 2 + ? `1 Transaction` + : `${transactionGroup.groupItems.length} Transactions` + }} +
=> { return commonRequestHandler(async () => { @@ -35,6 +37,7 @@ export const submitTransactionGroup = async ( description, atomic, sequential, + groupValidStart, groupItems, }, { diff --git a/front-end/src/renderer/services/transactionGroupsService.ts b/front-end/src/renderer/services/transactionGroupsService.ts index d44b62f92..c3e46325e 100644 --- a/front-end/src/renderer/services/transactionGroupsService.ts +++ b/front-end/src/renderer/services/transactionGroupsService.ts @@ -51,9 +51,10 @@ export const addGroupWithDrafts = async ( description: string, atomic: boolean, groupItems: StoreGroupItem[], + groupValidStart: Date, details?: string, ) => { - const group = await addGroup(description, atomic); + const group = await addGroup(description, atomic, groupValidStart); try { for (const [index, item] of groupItems.entries()) { const transactionDraft: Prisma.TransactionDraftUncheckedCreateInput = { @@ -96,12 +97,13 @@ export async function addGroupItem( } } -export async function addGroup(description: string, atomic: boolean) { +export async function addGroup(description: string, atomic: boolean, groupValidStart: Date) { const transactionGroup: Prisma.TransactionGroupUncheckedCreateInput = { description, // Not sure how this should be entered or what it's for atomic: atomic, created_at: new Date(), + groupValidStart: groupValidStart, }; try { return await window.electronAPI.local.transactionGroups.addGroup(transactionGroup); diff --git a/front-end/src/renderer/stores/storeTransactionGroup.ts b/front-end/src/renderer/stores/storeTransactionGroup.ts index b6b15347b..5f368bd2a 100644 --- a/front-end/src/renderer/stores/storeTransactionGroup.ts +++ b/front-end/src/renderer/stores/storeTransactionGroup.ts @@ -32,6 +32,7 @@ export interface GroupItem { const useTransactionGroupStore = defineStore('transactionGroup', () => { /* State */ const groupItems = ref([]); + const groupValidStart = ref(new Date()); const description = ref(''); const sequential = ref(false); const modified = ref(false); @@ -42,9 +43,18 @@ const useTransactionGroupStore = defineStore('transactionGroup', () => { if (modified.value) { return; } + groupItems.value = []; const group = await getGroup(id); description.value = group.description; + if (group.groupValidStart) { + if (group.groupValidStart > groupValidStart.value) { + groupValidStart.value = group.groupValidStart; + } else { + groupValidStart.value = new Date(); + } + } + const items = await getGroupItems(id); const drafts = await getDrafts(findArgs); const groupItemsToAdd: GroupItem[] = []; @@ -73,6 +83,7 @@ const useTransactionGroupStore = defineStore('transactionGroup', () => { function clearGroup() { groupItems.value = []; + groupValidStart.value = new Date(); description.value = ''; sequential.value = false; modified.value = false; @@ -161,10 +172,16 @@ const useTransactionGroupStore = defineStore('transactionGroup', () => { return new Date(validStartMillis); } - async function saveGroup(userId: string, description: string) { + async function saveGroup(userId: string, description: string, groupValidStart: Date) { // Alter this when we know what 'atomic' does if (groupItems.value[0].groupId === undefined) { - const newGroupId = await addGroupWithDrafts(userId, description, false, groupItems.value); + const newGroupId = await addGroupWithDrafts( + userId, + description, + false, + groupItems.value, + groupValidStart, + ); const items = await getGroupItems(newGroupId!); for (const [index, groupItem] of groupItems.value.entries()) { groupItem.groupId = newGroupId; @@ -174,7 +191,7 @@ const useTransactionGroupStore = defineStore('transactionGroup', () => { await updateGroup( groupItems.value[0].groupId, userId, - { description, atomic: false }, + { description, atomic: false, groupValidStart: groupValidStart }, groupItems.value, ); } @@ -221,6 +238,28 @@ const useTransactionGroupStore = defineStore('transactionGroup', () => { return modified.value; } + function updateTransactionValidStarts(newGroupValidStart: Date) { + groupItems.value = groupItems.value.map((item, index) => { + const now = new Date(); + if (item.validStart < now) { + const updatedValidStart = findUniqueValidStart( + item.payerAccountId, + newGroupValidStart.getTime() + index, + ); + const transaction = Transaction.fromBytes(item.transactionBytes); + transaction.setTransactionId(createTransactionId(item.payerAccountId, updatedValidStart)); + + return { + ...item, + transactionBytes: transaction.toBytes(), + validStart: updatedValidStart, + }; + } + return item; + }); + setModified(); + } + // function getObservers() { // const observers = new Array(); // for (const groupItem of groupItems.value) { @@ -244,6 +283,7 @@ const useTransactionGroupStore = defineStore('transactionGroup', () => { clearGroup, groupItems, description, + groupValidStart, sequential, getRequiredKeys, editGroupItem, @@ -251,6 +291,7 @@ const useTransactionGroupStore = defineStore('transactionGroup', () => { hasApprovers, setModified, isModified, + updateTransactionValidStarts, }; }); diff --git a/front-end/src/tests/main/services/localUser/transactionGroups.spec.ts b/front-end/src/tests/main/services/localUser/transactionGroups.spec.ts index c55ee3ac9..19c383c51 100644 --- a/front-end/src/tests/main/services/localUser/transactionGroups.spec.ts +++ b/front-end/src/tests/main/services/localUser/transactionGroups.spec.ts @@ -67,6 +67,7 @@ describe('Transaction Groups Service', () => { description: 'description', atomic: false, created_at: new Date(), + groupValidStart: new Date(), }; prisma.transactionGroup.findUnique.mockResolvedValue(group); @@ -166,6 +167,7 @@ describe('Transaction Groups Service', () => { const updateData = { description: 'new description', atomic: true, + groupValidDate: new Date(), }; await updateGroup(id, updateData);