diff --git a/back-end/apps/api/src/transactions/approvers/approvers.service.ts b/back-end/apps/api/src/transactions/approvers/approvers.service.ts index db21a0adf..e33ebee05 100644 --- a/back-end/apps/api/src/transactions/approvers/approvers.service.ts +++ b/back-end/apps/api/src/transactions/approvers/approvers.service.ts @@ -576,10 +576,9 @@ export class ApproversService { /* Check if the transaction exists */ if (!transaction) throw new BadRequestException(ErrorCodes.TNF); - /* Checks if the transaction is executed */ + /* Checks if the transaction is requires approval */ if ( transaction.status !== TransactionStatus.WAITING_FOR_SIGNATURES && - transaction.status !== TransactionStatus.SIGN_ONLY && transaction.status !== TransactionStatus.WAITING_FOR_EXECUTION ) throw new BadRequestException(ErrorCodes.TNRA); diff --git a/back-end/apps/api/src/transactions/dto/create-transaction.dto.ts b/back-end/apps/api/src/transactions/dto/create-transaction.dto.ts index a154e316c..56f0d1d15 100644 --- a/back-end/apps/api/src/transactions/dto/create-transaction.dto.ts +++ b/back-end/apps/api/src/transactions/dto/create-transaction.dto.ts @@ -34,5 +34,5 @@ export class CreateTransactionDto { @IsOptional() @IsBoolean() - isSignOnly?: boolean; + isManual?: boolean; } diff --git a/back-end/apps/api/src/transactions/dto/transaction.dto.ts b/back-end/apps/api/src/transactions/dto/transaction.dto.ts index 26923e1f7..5e771c6cf 100644 --- a/back-end/apps/api/src/transactions/dto/transaction.dto.ts +++ b/back-end/apps/api/src/transactions/dto/transaction.dto.ts @@ -40,6 +40,9 @@ export class TransactionDto { @Expose() validStart: Date; + @Expose() + isManual: boolean; + @Expose() cutoffAt?: Date; diff --git a/back-end/apps/api/src/transactions/signers/signers.service.ts b/back-end/apps/api/src/transactions/signers/signers.service.ts index 09eb639c5..315bf8ae6 100644 --- a/back-end/apps/api/src/transactions/signers/signers.service.ts +++ b/back-end/apps/api/src/transactions/signers/signers.service.ts @@ -110,15 +110,13 @@ export class SignersService { /* Checks if the transaction is canceled */ if ( transaction.status !== TransactionStatus.WAITING_FOR_SIGNATURES && - transaction.status !== TransactionStatus.SIGN_ONLY && transaction.status !== TransactionStatus.WAITING_FOR_EXECUTION ) throw new BadRequestException(ErrorCodes.TNRS); /* Checks if the transaction is expired */ let sdkTransaction = SDKTransaction.fromBytes(transaction.transactionBytes); - if (isExpired(sdkTransaction) && transaction.status !== TransactionStatus.SIGN_ONLY) - throw new BadRequestException(ErrorCodes.TE); + if (isExpired(sdkTransaction)) throw new BadRequestException(ErrorCodes.TE); /* Validates the signatures */ const { data: publicKeys, error } = safe( diff --git a/back-end/apps/api/src/transactions/transactions.controller.spec.ts b/back-end/apps/api/src/transactions/transactions.controller.spec.ts index bf46a118f..b9dfcdcb1 100644 --- a/back-end/apps/api/src/transactions/transactions.controller.spec.ts +++ b/back-end/apps/api/src/transactions/transactions.controller.spec.ts @@ -83,6 +83,7 @@ describe('TransactionsController', () => { ), status: TransactionStatus.NEW, mirrorNetwork: 'testnet', + isManual: false, cutoffAt: new Date(), createdAt: new Date(), updatedAt: new Date(), @@ -297,25 +298,6 @@ describe('TransactionsController', () => { }); }); - describe('markAsSignOnlyTransaction', () => { - it('should return a boolean indicating if the transaction has been marked as sign only', async () => { - const result = true; - transactionService.markAsSignOnlyTransaction.mockResolvedValue(result); - - expect(await controller.markAsSignOnlyTransaction(user, 1)).toBe(result); - }); - - it('should return a boolean indicating if the transaction has not been marked as sign only', async () => { - jest - .spyOn(controller, 'markAsSignOnlyTransaction') - .mockRejectedValue(new BadRequestException('Transaction cannot be marked as sign only')); - - await expect(controller.markAsSignOnlyTransaction(user, 1)).rejects.toThrow( - 'Transaction cannot be marked as sign only', - ); - }); - }); - describe('archiveTransaction', () => { it('should return a boolean indicating if the transaction has been archiveed', async () => { const result = true; diff --git a/back-end/apps/api/src/transactions/transactions.controller.ts b/back-end/apps/api/src/transactions/transactions.controller.ts index 35551a582..8da5ca83a 100644 --- a/back-end/apps/api/src/transactions/transactions.controller.ts +++ b/back-end/apps/api/src/transactions/transactions.controller.ts @@ -210,36 +210,35 @@ export class TransactionsController { } @ApiOperation({ - summary: 'Marks the transaction as sign-only', - description: - 'Marks a transaction as sign-only, meaning it would not be executed. Only signatures will be gathered.', + summary: 'Archives a transaction', + description: 'Archive a transaction that is marked as sign only', }) @ApiResponse({ status: 200, type: Boolean, }) - @Patch('/mark-sign-only/:id') - async markAsSignOnlyTransaction( + @Patch('/archive/:id') + async archiveTransaction( @GetUser() user, @Param('id', ParseIntPipe) id: number, ): Promise { - return this.transactionsService.markAsSignOnlyTransaction(id, user); + return this.transactionsService.archiveTransaction(id, user); } @ApiOperation({ - summary: 'Archives a transaction', - description: 'Archive a transaction that is marked as sign only', + summary: 'Send a transaction for execution', + description: 'Send a manual transaction to the chain service that will execute it', }) @ApiResponse({ status: 200, type: Boolean, }) - @Patch('/archive/:id') - async archiveTransaction( + @Patch('/execute/:id') + async executeTransaction( @GetUser() user, @Param('id', ParseIntPipe) id: number, ): Promise { - return this.transactionsService.archiveTransaction(id, user); + return this.transactionsService.executeTransaction(id, user); } @ApiOperation({ diff --git a/back-end/apps/api/src/transactions/transactions.service.spec.ts b/back-end/apps/api/src/transactions/transactions.service.spec.ts index d7ee86c8e..75a3cebcc 100644 --- a/back-end/apps/api/src/transactions/transactions.service.spec.ts +++ b/back-end/apps/api/src/transactions/transactions.service.spec.ts @@ -20,7 +20,7 @@ import { Client, } from '@hashgraph/sdk'; -import { NOTIFICATIONS_SERVICE, MirrorNodeService, ErrorCodes } from '@app/common'; +import { NOTIFICATIONS_SERVICE, MirrorNodeService, ErrorCodes, CHAIN_SERVICE } from '@app/common'; import { attachKeys, getClientFromNetwork, @@ -31,6 +31,7 @@ import { notifySyncIndicators, MirrorNetworkGRPC, isTransactionBodyOverMaxSize, + emitExecuteTranasction, } from '@app/common/utils'; import { Transaction, @@ -52,6 +53,7 @@ describe('TransactionsService', () => { let service: TransactionsService; const transactionsRepo = mockDeep>(); + const chainService = mock(); const notificationsService = mock(); const approversService = mock(); const mirrorNodeService = mock(); @@ -89,6 +91,10 @@ describe('TransactionsService', () => { provide: NOTIFICATIONS_SERVICE, useValue: notificationsService, }, + { + provide: CHAIN_SERVICE, + useValue: chainService, + }, { provide: ApproversService, useValue: approversService, @@ -427,7 +433,7 @@ describe('TransactionsService', () => { client.close(); }); - it('should create a sign-only transaction', async () => { + it('should create a manual transaction', async () => { const sdkTransaction = new AccountCreateTransaction().setTransactionId( new TransactionId(AccountId.fromString('0.0.1'), Timestamp.fromDate(new Date())), ); @@ -439,7 +445,7 @@ describe('TransactionsService', () => { creatorKeyId: 1, signature: Buffer.from('0xabc02'), mirrorNetwork: 'testnet', - isSignOnly: true, + isManual: true, }; const client = Client.forTestnet(); @@ -464,7 +470,7 @@ describe('TransactionsService', () => { await service.createTransaction(dto, user as User); expect(transactionsRepo.save).toHaveBeenCalledWith( - expect.objectContaining({ status: TransactionStatus.SIGN_ONLY }), + expect.objectContaining({ isManual: true }), ); expect(notifyWaitingForSignatures).toHaveBeenCalledWith(notificationsService, 1); expect(notifyTransactionAction).toHaveBeenCalledWith(notificationsService); @@ -668,97 +674,98 @@ describe('TransactionsService', () => { }); }); - describe('markAsSignOnlyTransaction', () => { + describe('archiveTransaction', () => { beforeEach(() => { jest.resetAllMocks(); }); - it('should throw if transaction status is not in progress', async () => { + it('should throw if transaction status is not archiveable', async () => { const transaction = { creatorKey: { userId: 1 }, - status: TransactionStatus.EXECUTED, + status: TransactionStatus.CANCELED, }; jest .spyOn(service, 'getTransactionForCreator') .mockResolvedValueOnce(transaction as Transaction); - await expect(service.markAsSignOnlyTransaction(123, { id: 1 } as User)).rejects.toThrow( - ErrorCodes.OTIP, + await expect(service.archiveTransaction(123, { id: 1 } as User)).rejects.toThrow( + ErrorCodes.OMTIP, ); }); - it('should update transaction status to SIGN_ONLY and return true', async () => { + it('should update transaction status to ARCHIVED and return true', async () => { const transaction = { id: 123, creatorKey: { userId: 1 }, - status: TransactionStatus.WAITING_FOR_SIGNATURES, + isManual: true, + status: TransactionStatus.WAITING_FOR_EXECUTION, }; jest .spyOn(service, 'getTransactionForCreator') .mockResolvedValueOnce(transaction as Transaction); - const result = await service.markAsSignOnlyTransaction(123, { id: 1 } as User); + const result = await service.archiveTransaction(123, { id: 1 } as User); expect(transactionsRepo.update).toHaveBeenCalledWith( { id: 123 }, - { status: TransactionStatus.SIGN_ONLY }, + { status: TransactionStatus.ARCHIVED }, ); expect(result).toBe(true); expect(notifySyncIndicators).toHaveBeenCalledWith( notificationsService, transaction.id, - TransactionStatus.SIGN_ONLY, + TransactionStatus.ARCHIVED, ); expect(notifyTransactionAction).toHaveBeenCalledWith(notificationsService); }); }); - describe('archiveTransaction', () => { + describe('executeTransaction', () => { beforeEach(() => { jest.resetAllMocks(); }); - it('should throw if transaction status is not archiveable', async () => { + it('should throw if transaction is not manual', async () => { const transaction = { - creatorKey: { userId: 1 }, - status: TransactionStatus.WAITING_FOR_SIGNATURES, //Only SIGN_ONLY transactions can be archived + id: 123, + creatorKey: { userId: user.id }, + isManual: false, }; jest .spyOn(service, 'getTransactionForCreator') .mockResolvedValueOnce(transaction as Transaction); - await expect(service.archiveTransaction(123, { id: 1 } as User)).rejects.toThrow( - ErrorCodes.OSONT, - ); + await expect(service.executeTransaction(123, user as User)).rejects.toThrow(ErrorCodes.IO); }); - it('should update transaction status to ARCHIVED and return true', async () => { + it('should emit execute transaction event and return true if transaction is manual', async () => { const transaction = { id: 123, - creatorKey: { userId: 1 }, - status: TransactionStatus.SIGN_ONLY, + creatorKey: { userId: user.id }, + isManual: true, + status: TransactionStatus.WAITING_FOR_EXECUTION, + transactionBytes: Buffer.from('transactionBytes'), + mirrorNetwork: 'testnet', + validStart: new Date(), }; jest .spyOn(service, 'getTransactionForCreator') .mockResolvedValueOnce(transaction as Transaction); - const result = await service.archiveTransaction(123, { id: 1 } as User); + const result = await service.executeTransaction(123, user as User); - expect(transactionsRepo.update).toHaveBeenCalledWith( - { id: 123 }, - { status: TransactionStatus.ARCHIVED }, - ); expect(result).toBe(true); - expect(notifySyncIndicators).toHaveBeenCalledWith( - notificationsService, - transaction.id, - TransactionStatus.ARCHIVED, - ); - expect(notifyTransactionAction).toHaveBeenCalledWith(notificationsService); + expect(emitExecuteTranasction).toHaveBeenCalledWith(chainService, { + id: transaction.id, + status: transaction.status, + transactionBytes: transaction.transactionBytes.toString('hex'), + mirrorNetwork: transaction.mirrorNetwork, + validStart: transaction.validStart, + }); }); }); @@ -936,6 +943,7 @@ describe('TransactionsService', () => { ); }); }); + describe('shouldApproveTransaction', () => { beforeEach(() => { jest.resetAllMocks(); diff --git a/back-end/apps/api/src/transactions/transactions.service.ts b/back-end/apps/api/src/transactions/transactions.service.ts index eb7825d37..b285a28e5 100644 --- a/back-end/apps/api/src/transactions/transactions.service.ts +++ b/back-end/apps/api/src/transactions/transactions.service.ts @@ -18,6 +18,7 @@ import { import { Transaction, TransactionSigner, TransactionStatus, User } from '@entities'; import { + CHAIN_SERVICE, NOTIFICATIONS_SERVICE, MirrorNodeService, encodeUint8Array, @@ -37,6 +38,7 @@ import { notifySyncIndicators, ErrorCodes, isTransactionBodyOverMaxSize, + emitExecuteTranasction, } from '@app/common'; import { CreateTransactionDto } from './dto'; @@ -48,6 +50,7 @@ export class TransactionsService { constructor( @InjectRepository(Transaction) private repo: Repository, @Inject(NOTIFICATIONS_SERVICE) private readonly notificationsService: ClientProxy, + @Inject(CHAIN_SERVICE) private readonly chainService: ClientProxy, private readonly approversService: ApproversService, private readonly mirrorNodeService: MirrorNodeService, @InjectEntityManager() private entityManager: EntityManager, @@ -330,7 +333,7 @@ export class TransactionsService { /* Check if the transaction is expired */ const sdkTransaction = SDKTransaction.fromBytes(dto.transactionBytes); - if (isExpired(sdkTransaction) && !dto.isSignOnly) throw new BadRequestException(ErrorCodes.TE); + if (isExpired(sdkTransaction)) throw new BadRequestException(ErrorCodes.TE); /* Check if the transaction body is over the max size */ if (isTransactionBodyOverMaxSize(sdkTransaction)) { @@ -373,14 +376,11 @@ export class TransactionsService { signature: dto.signature, mirrorNetwork: dto.mirrorNetwork, validStart: sdkTransaction.transactionId.validStart.toDate(), + isManual: dto.isManual, cutoffAt: dto.cutoffAt, }); client.close(); - if (dto.isSignOnly) { - transaction.status = TransactionStatus.SIGN_ONLY; - } - try { await this.repo.save(transaction); } catch { @@ -418,7 +418,6 @@ export class TransactionsService { TransactionStatus.NEW, TransactionStatus.WAITING_FOR_SIGNATURES, TransactionStatus.WAITING_FOR_EXECUTION, - TransactionStatus.SIGN_ONLY, ].includes(transaction.status) ) { throw new BadRequestException(ErrorCodes.OTIP); @@ -432,41 +431,43 @@ export class TransactionsService { return true; } - /* Mark the transaction as sign only. */ - async markAsSignOnlyTransaction(id: number, user: User): Promise { + /* Archive the transaction if the transaction is sign only. */ + async archiveTransaction(id: number, user: User): Promise { const transaction = await this.getTransactionForCreator(id, user); if ( - ![ - TransactionStatus.NEW, - TransactionStatus.WAITING_FOR_SIGNATURES, - TransactionStatus.WAITING_FOR_EXECUTION, - TransactionStatus.SIGN_ONLY, - ].includes(transaction.status) + ![TransactionStatus.WAITING_FOR_SIGNATURES, TransactionStatus.WAITING_FOR_EXECUTION].includes( + transaction.status, + ) && + !transaction.isManual ) { - throw new BadRequestException(ErrorCodes.OTIP); + throw new BadRequestException(ErrorCodes.OMTIP); } - await this.repo.update({ id }, { status: TransactionStatus.SIGN_ONLY }); + await this.repo.update({ id }, { status: TransactionStatus.ARCHIVED }); - notifySyncIndicators(this.notificationsService, transaction.id, TransactionStatus.SIGN_ONLY); + notifySyncIndicators(this.notificationsService, transaction.id, TransactionStatus.ARCHIVED); notifyTransactionAction(this.notificationsService); return true; } - /* Archive the transaction if the transaction is sign only. */ - async archiveTransaction(id: number, user: User): Promise { + /* Sends the transaction for execution if the transaction is manual. */ + async executeTransaction(id: number, user: User): Promise { const transaction = await this.getTransactionForCreator(id, user); - if (![TransactionStatus.SIGN_ONLY].includes(transaction.status)) { - throw new BadRequestException(ErrorCodes.OSONT); + if (!transaction.isManual) { + throw new BadRequestException(ErrorCodes.IO); } - await this.repo.update({ id }, { status: TransactionStatus.ARCHIVED }); - - notifySyncIndicators(this.notificationsService, transaction.id, TransactionStatus.ARCHIVED); - notifyTransactionAction(this.notificationsService); + emitExecuteTranasction(this.chainService, { + id: transaction.id, + status: transaction.status, + //@ts-expect-error - cannot send Buffer instance over the network + transactionBytes: transaction.transactionBytes.toString('hex'), + mirrorNetwork: transaction.mirrorNetwork, + validStart: transaction.validStart, + }); return true; } diff --git a/back-end/apps/api/test/spec/transaction-signers.e2e-spec.ts b/back-end/apps/api/test/spec/transaction-signers.e2e-spec.ts index 19389e4a0..0f1b3015f 100644 --- a/back-end/apps/api/test/spec/transaction-signers.e2e-spec.ts +++ b/back-end/apps/api/test/spec/transaction-signers.e2e-spec.ts @@ -108,58 +108,6 @@ describe('Transactions (e2e)', () => { ]); }); - it('(POST) should upload a signature map for a transaction that is expired but sign-only', async () => { - let sdkTransaction = getExpiredTransaction(localnet1003.accountId); - const buffer = Buffer.from(sdkTransaction.toBytes()).toString('hex'); - - const createRes = await endpoint.post( - { - name: 'TEST Simple Account Create Transaction', - description: 'TEST This is a simple account create transaction', - transactionBytes: buffer, - creatorKeyId: userKey1003.id, - signature: Buffer.from(localnet1003.privateKey.sign(sdkTransaction.toBytes())).toString( - 'hex', - ), - mirrorNetwork: localnet1003.mirrorNetwork, - }, - '', - userAuthToken, - ); - expect(createRes.status).toBe(201); - - const getRes = await endpoint.get(`/${createRes.body.id}`, userAuthToken); - expect(getRes.status).toBe(200); - - sdkTransaction = AccountCreateTransaction.fromBytes( - Buffer.from(getRes.body.transactionBytes, 'hex'), - ); - - await sdkTransaction.sign(localnet1003.privateKey); - const signatureMap = getSignatureMapForPublicKeys( - [localnet1003.publicKeyRaw], - sdkTransaction, - ); - - const response = await endpoint.post( - { - signatureMap: formatSignatureMap(signatureMap), - }, - `/${createRes.body.id}/signers`, - userAuthToken, - ); - - const signerEntry = await transactionSignerRepo.findOne({ - where: { - transactionId: createRes.body.id.id, - userKeyId: userKey1003.id, - }, - }); - - expect(signerEntry).toBeDefined(); - expect(response.status).toBe(201); - }); - it('(POST) should upload a signature map 2 public keys for a transaction', async () => { const sdkTransaction = new AccountUpdateTransaction() .setTransactionId(createTransactionId(localnet1003.accountId)) diff --git a/back-end/apps/api/test/spec/transaction.e2e-spec.ts b/back-end/apps/api/test/spec/transaction.e2e-spec.ts index a62dde0a2..0b4d3e484 100644 --- a/back-end/apps/api/test/spec/transaction.e2e-spec.ts +++ b/back-end/apps/api/test/spec/transaction.e2e-spec.ts @@ -137,11 +137,11 @@ describe('Transactions (e2e)', () => { ); }); - it("(POST) should create a transaction marked as 'sign only'", async () => { + it('(POST) should create a manual transaction', async () => { const transaction = await createTransaction(); const { status, body } = await endpoint.post( - { ...transaction, isSignOnly: true }, + { ...transaction, isManual: true }, null, userAuthToken, ); @@ -153,8 +153,9 @@ describe('Transactions (e2e)', () => { expect.objectContaining({ name: transaction.name, description: transaction.description, - status: TransactionStatus.SIGN_ONLY, + status: TransactionStatus.WAITING_FOR_SIGNATURES, creatorKeyId: transaction.creatorKeyId, + isManual: true, }), ); }); @@ -188,45 +189,6 @@ describe('Transactions (e2e)', () => { ); }); - it('(POST) should create a transaction with an ID of an archived transaction', async () => { - const transaction = await createTransaction(); - - const { body: newTransaction } = await endpoint.post(transaction, null, userAuthToken); - testsAddedTransactionsCountUser++; - - const markAsSignOnlyEndpoint = new Endpoint(server, '/transactions/mark-sign-only'); - const markAsSignOnlyRes = await markAsSignOnlyEndpoint.patch( - null, - newTransaction.id.toString(), - userAuthToken, - ); - expect(markAsSignOnlyRes.status).toEqual(200); - - const archiveEndpoint = new Endpoint(server, '/transactions/archive'); - const archiveRes = await archiveEndpoint.patch( - null, - newTransaction.id.toString(), - userAuthToken, - ); - console.log(archiveRes.body); - - expect(archiveRes.status).toEqual(200); - - const { status, body } = await endpoint.post(transaction, null, userAuthToken); - testsAddedTransactionsCountUser++; - - expect(status).toEqual(201); - expect(body.transactionBytes).not.toEqual(transaction.transactionBytes); - expect(body).toMatchObject( - expect.objectContaining({ - name: transaction.name, - description: transaction.description, - status: TransactionStatus.WAITING_FOR_SIGNATURES, - creatorKeyId: transaction.creatorKeyId, - }), - ); - }); - it('(POST) should not create a transaction if user is not verified', async () => { const countBefore = await repo.count(); await endpoint.post(await createTransaction(), null, userNewAuthToken).expect(403); @@ -887,28 +849,12 @@ describe('Transactions (e2e)', () => { endpoint = new Endpoint(server, '/transactions/archive'); }); - afterAll(async () => { - await resetDatabase(); - await addHederaLocalnetAccounts(); - testsAddedTransactionsCountUser = 0; - testsAddedTransactionsCountAdmin = 0; - addedTransactions = await addTransactions(); - }); - it('(PATCH) should archive a transaction if creator', async () => { const transaction = await createTransaction(user, localnet1003); const { body: newTransaction } = await new Endpoint(server, '/transactions') - .post(transaction, null, userAuthToken) + .post({ ...transaction, isManual: true }, null, userAuthToken) .expect(201); - const markAsSignOnlyEndpoint = new Endpoint(server, '/transactions/mark-sign-only'); - const markAsSignOnlyRes = await markAsSignOnlyEndpoint.patch( - null, - newTransaction.id.toString(), - userAuthToken, - ); - expect(markAsSignOnlyRes.status).toEqual(200); - const { status } = await endpoint.patch(null, newTransaction.id.toString(), userAuthToken); const transactionFromDb = await repo.findOne({ where: { id: newTransaction.id } }); @@ -932,33 +878,86 @@ describe('Transactions (e2e)', () => { }); it('(PATCH) should not archive a transaction if not verified', async () => { - const transaction = await createTransaction(user, localnet1003); - const { body: newTransaction } = await new Endpoint(server, '/transactions') - .post(transaction, null, userAuthToken) - .expect(201); + await endpoint.patch(null, '2', userNewAuthToken).expect(403); + }); - const { status } = await endpoint.patch(null, newTransaction.id.toString(), userNewAuthToken); + it("(PATCH) should not archive a transaction if it's already executed", async () => { + const transaction = addedTransactions.userTransactions[0]; + const oldStatus = transaction.status; + await repo.update({ id: transaction.id }, { status: TransactionStatus.EXECUTED }); - const transactionFromDb = await repo.findOne({ where: { id: newTransaction.id } }); + const { status } = await endpoint.patch(null, transaction.id.toString(), userAuthToken); - expect(status).toEqual(403); + const transactionFromDb = await repo.findOne({ where: { id: transaction.id } }); + + expect(status).toEqual(400); expect(transactionFromDb?.status).not.toEqual(TransactionStatus.ARCHIVED); + + await repo.update({ id: transaction.id }, { status: oldStatus }); }); + }); - it("(PATCH) should not archive a transaction if it's already executed", async () => { + describe('/transactions/execute/:transactionId', () => { + let endpoint: Endpoint; + + beforeAll(() => { + endpoint = new Endpoint(server, '/transactions/execute'); + }); + + it('(PATCH) should emit execute event to chain if creator', async () => { + const transactionsEndpoint = new Endpoint(server, '/transactions'); const transaction = await createTransaction(user, localnet1003); - const { body: newTransaction } = await new Endpoint(server, '/transactions') - .post(transaction, null, userAuthToken) + const { body: newTransaction } = await transactionsEndpoint + .post({ ...transaction, isManual: true }, null, userAuthToken) .expect(201); - await repo.update({ id: newTransaction.id }, { status: TransactionStatus.EXECUTED }); + const sdkTransaction = AccountCreateTransaction.fromBytes( + Buffer.from(newTransaction.transactionBytes, 'hex'), + ); + await sdkTransaction.sign(localnet1003.privateKey); + const signatureMap = getSignatureMapForPublicKeys( + [localnet1003.publicKeyRaw], + sdkTransaction, + ); - const { status } = await endpoint.patch(null, newTransaction.id.toString(), userAuthToken); + console.log(); - const transactionFromDb = await repo.findOne({ where: { id: newTransaction.id } }); + await transactionsEndpoint + .post( + { signatureMap: formatSignatureMap(signatureMap) }, + `/${newTransaction.id}/signers`, + userAuthToken, + ) + .expect(201); - expect(status).toEqual(400); - expect(transactionFromDb?.status).not.toEqual(TransactionStatus.ARCHIVED); + await endpoint.patch(null, newTransaction.id.toString(), userAuthToken).expect(200); + }); + + it('(PATCH) should fail if not creator', async () => { + const transactionsEndpoint = new Endpoint(server, '/transactions'); + const transaction = await createTransaction(user, localnet1003); + const { body: newTransaction } = await transactionsEndpoint + .post({ ...transaction, isManual: true }, null, userAuthToken) + .expect(201); + + await endpoint.patch(null, newTransaction.id.toString(), adminAuthToken).expect(401); + }); + + it('(PATCH) should fail if not a manual transaction', async () => { + const transactionsEndpoint = new Endpoint(server, '/transactions'); + const transaction = await createTransaction(user, localnet1003); + const { body: newTransaction } = await transactionsEndpoint + .post(transaction, null, userAuthToken) + .expect(201); + + const { body } = await endpoint + .patch(null, newTransaction.id.toString(), userAuthToken) + .expect(400); + expect(body).toMatchObject( + expect.objectContaining({ + code: ErrorCodes.IO, + }), + ); }); }); diff --git a/back-end/apps/chain/src/execute/execute.controller.spec.ts b/back-end/apps/chain/src/execute/execute.controller.spec.ts new file mode 100644 index 000000000..bc46f3fa6 --- /dev/null +++ b/back-end/apps/chain/src/execute/execute.controller.spec.ts @@ -0,0 +1,63 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { mockDeep } from 'jest-mock-extended'; + +import { TransactionStatus } from '@entities'; +import { ExecuteTransactionDto } from '@app/common'; + +import { ExecuteController } from './execute.controller'; +import { ExecuteService } from './execute.service'; + +describe('ExecuteControllerController', () => { + let controller: ExecuteController; + const executeService = mockDeep(); + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [ExecuteController], + providers: [ + { + provide: ExecuteService, + useValue: executeService, + }, + ], + }).compile(); + + controller = module.get(ExecuteController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); + + it('should invoke execute transaction in execute service with Buffer', async () => { + const payload: ExecuteTransactionDto = { + id: 1, + mirrorNetwork: 'testnet', + validStart: new Date(), + transactionBytes: Buffer.from('test'), + status: TransactionStatus.WAITING_FOR_EXECUTION, + }; + + await controller.executeTransaction(payload); + + expect(executeService.executeTransaction).toHaveBeenCalledWith(payload); + }); + + it('should invoke execute transaction in execute service with Buffer', async () => { + const payload: ExecuteTransactionDto = { + id: 1, + mirrorNetwork: 'testnet', + validStart: new Date(), + //@ts-expect-error support hex string + transactionBytes: '0xabc', + status: TransactionStatus.WAITING_FOR_EXECUTION, + }; + + await controller.executeTransaction(payload); + + expect(executeService.executeTransaction).toHaveBeenCalledWith({ + ...payload, + transactionBytes: Buffer.from('abc', 'hex'), + }); + }); +}); diff --git a/back-end/apps/chain/src/execute/execute.controller.ts b/back-end/apps/chain/src/execute/execute.controller.ts new file mode 100644 index 000000000..06c62a808 --- /dev/null +++ b/back-end/apps/chain/src/execute/execute.controller.ts @@ -0,0 +1,20 @@ +import { Controller } from '@nestjs/common'; +import { EventPattern, Payload } from '@nestjs/microservices'; + +import { decode, EXECUTE_TRANSACTION, ExecuteTransactionDto } from '@app/common'; + +import { ExecuteService } from './execute.service'; + +@Controller('execute') +export class ExecuteController { + constructor(private readonly executeService: ExecuteService) {} + + @EventPattern(EXECUTE_TRANSACTION) + async executeTransaction(@Payload() payload: ExecuteTransactionDto) { + if (typeof payload.transactionBytes === 'string') { + payload.transactionBytes = decode(payload.transactionBytes); + } + + return this.executeService.executeTransaction(payload); + } +} diff --git a/back-end/apps/chain/src/execute/execute.module.ts b/back-end/apps/chain/src/execute/execute.module.ts index 24cbc7bc6..324274c17 100644 --- a/back-end/apps/chain/src/execute/execute.module.ts +++ b/back-end/apps/chain/src/execute/execute.module.ts @@ -18,6 +18,7 @@ import { import { MirrorNodeModule, RedisMurlockModule } from '@app/common'; +import { ExecuteController } from './execute.controller'; import { ExecuteService } from './execute.service'; @Module({ @@ -39,7 +40,7 @@ import { ExecuteService } from './execute.service'; MirrorNodeModule, RedisMurlockModule, ], - controllers: [], + controllers: [ExecuteController], providers: [ExecuteService], exports: [ExecuteService], }) diff --git a/back-end/apps/chain/src/execute/execute.service.spec.ts b/back-end/apps/chain/src/execute/execute.service.spec.ts index c79b263c1..a0f81c218 100644 --- a/back-end/apps/chain/src/execute/execute.service.spec.ts +++ b/back-end/apps/chain/src/execute/execute.service.spec.ts @@ -409,19 +409,14 @@ describe('ExecuteService', () => { 'Transaction has been canceled.', ); - transaction.status = TransactionStatus.SIGN_ONLY; - transactionRepo.findOne.mockResolvedValueOnce(transaction); - - await expect(service.executeTransaction(transaction)).rejects.toThrow( - 'Transaction is sign-only.', - ); - transaction.status = TransactionStatus.ARCHIVED; transactionRepo.findOne.mockResolvedValueOnce(transaction); await expect(service.executeTransaction(transaction)).rejects.toThrow( 'Transaction is archived.', ); + + transaction.isManual = false; }); it('should throw if transaction is null or undefined', async () => { diff --git a/back-end/apps/chain/src/execute/execute.service.ts b/back-end/apps/chain/src/execute/execute.service.ts index 59e6dfc38..61937d6f2 100644 --- a/back-end/apps/chain/src/execute/execute.service.ts +++ b/back-end/apps/chain/src/execute/execute.service.ts @@ -183,8 +183,6 @@ export class ExecuteService { throw new Error('Transaction has been expired.'); case TransactionStatus.CANCELED: throw new Error('Transaction has been canceled.'); - case TransactionStatus.SIGN_ONLY: - throw new Error('Transaction is sign-only.'); case TransactionStatus.ARCHIVED: throw new Error('Transaction is archived.'); } diff --git a/back-end/apps/chain/src/transaction-status/transaction-status.service.spec.ts b/back-end/apps/chain/src/transaction-status/transaction-status.service.spec.ts index 5e2b9ab0f..42cd0c9ee 100644 --- a/back-end/apps/chain/src/transaction-status/transaction-status.service.spec.ts +++ b/back-end/apps/chain/src/transaction-status/transaction-status.service.spec.ts @@ -1167,6 +1167,16 @@ describe('TransactionStatusService', () => { ); }); + it('should not a timeout if the transaction is manual', () => { + transaction.isManual = true; + + service.addExecutionTimeout(transaction); + + expect(setTimeout).not.toHaveBeenCalled(); + + transaction.isManual = false; + }); + it('should add the timeout to the scheduler registry', () => { service.addExecutionTimeout(transaction); diff --git a/back-end/apps/chain/src/transaction-status/transaction-status.service.ts b/back-end/apps/chain/src/transaction-status/transaction-status.service.ts index e6d245305..b1f4c81fa 100644 --- a/back-end/apps/chain/src/transaction-status/transaction-status.service.ts +++ b/back-end/apps/chain/src/transaction-status/transaction-status.service.ts @@ -204,7 +204,7 @@ export class TransactionStatusService { transaction: Transaction, ): Promise { /* Returns if the transaction is null */ - if (!transaction || transaction.status === TransactionStatus.SIGN_ONLY) return; + if (!transaction) return; /* Gets the SDK transaction from the transaction body */ const sdkTransaction = SDKTransaction.fromBytes(transaction.transactionBytes); @@ -423,6 +423,8 @@ export class TransactionStatusService { if (this.schedulerRegistry.doesExist('timeout', name)) return; + if (transaction.isManual) return; + const timeToValidStart = transaction.validStart.getTime() - Date.now(); const callback = async () => { diff --git a/back-end/apps/notifications/src/receiver/receiver.service.ts b/back-end/apps/notifications/src/receiver/receiver.service.ts index d6ba7a7de..81ab7b14b 100644 --- a/back-end/apps/notifications/src/receiver/receiver.service.ts +++ b/back-end/apps/notifications/src/receiver/receiver.service.ts @@ -119,7 +119,6 @@ export class ReceiverService { /* Determine new indicator type */ switch (transactionStatus) { case TransactionStatus.WAITING_FOR_SIGNATURES: - case TransactionStatus.SIGN_ONLY: newIndicatorType = NotificationType.TRANSACTION_INDICATOR_SIGN; break; case TransactionStatus.WAITING_FOR_EXECUTION: diff --git a/back-end/libs/common/src/constants/errorCodes.ts b/back-end/libs/common/src/constants/errorCodes.ts index 240a7d27e..28a62c031 100644 --- a/back-end/libs/common/src/constants/errorCodes.ts +++ b/back-end/libs/common/src/constants/errorCodes.ts @@ -9,7 +9,7 @@ export enum ErrorCodes { TEX = 'TEX', FST = 'FST', OTIP = 'OTIP', - OSONT = 'OSONT', + OMTIP = 'OMTIP', TNF = 'TNF', TAP = 'TAP', TAX = 'TAX', @@ -36,6 +36,7 @@ export enum ErrorCodes { NNF = 'NNF', IB = 'IB', TOS = 'TOS', + IO = 'IO', } export const ErrorMessages: { [key in ErrorCodes]: string } = { @@ -47,7 +48,7 @@ export const ErrorMessages: { [key in ErrorCodes]: string } = { [ErrorCodes.TEX]: 'Transaction already exists', [ErrorCodes.FST]: 'Failed to save transaction', [ErrorCodes.OTIP]: 'Only transactions in progress can be canceled', - [ErrorCodes.OSONT]: 'Only sign-only transactions can be archived', + [ErrorCodes.OMTIP]: 'Only manual transaction in progress can be archived', [ErrorCodes.TNF]: 'Transaction not found', [ErrorCodes.TAP]: 'Transaction already approved', [ErrorCodes.TAX]: 'Transaction already executed', @@ -75,4 +76,5 @@ export const ErrorMessages: { [key in ErrorCodes]: string } = { [ErrorCodes.NNF]: 'Notification not found', [ErrorCodes.IB]: 'Invalid body', [ErrorCodes.TOS]: 'Transaction is over the size limit and cannot be executed', + [ErrorCodes.IO]: 'Invalid operation', }; diff --git a/back-end/libs/common/src/constants/eventPatterns.ts b/back-end/libs/common/src/constants/eventPatterns.ts index b9092d3e3..045c866fe 100644 --- a/back-end/libs/common/src/constants/eventPatterns.ts +++ b/back-end/libs/common/src/constants/eventPatterns.ts @@ -13,3 +13,4 @@ export const SYNC_INDICATORS = 'sync_indicators'; /* Chain patterns */ export const UPDATE_TRANSACTION_STATUS = 'update_transaction_status'; +export const EXECUTE_TRANSACTION = 'execute_transaction'; diff --git a/back-end/libs/common/src/database/entities/transaction.entity.ts b/back-end/libs/common/src/database/entities/transaction.entity.ts index a942fc56e..7aa3d3127 100644 --- a/back-end/libs/common/src/database/entities/transaction.entity.ts +++ b/back-end/libs/common/src/database/entities/transaction.entity.ts @@ -42,7 +42,6 @@ export enum TransactionType { export enum TransactionStatus { NEW = 'NEW', // unused CANCELED = 'CANCELED', - SIGN_ONLY = 'SIGN_ONLY', REJECTED = 'REJECTED', WAITING_FOR_SIGNATURES = 'WAITING FOR SIGNATURES', WAITING_FOR_EXECUTION = 'WAITING FOR EXECUTION', @@ -107,6 +106,9 @@ export class Transaction { @Column() mirrorNetwork: string; + @Column({ default: false }) + isManual: boolean; + @Column({ nullable: true }) cutoffAt?: Date; diff --git a/back-end/libs/common/src/utils/client/index.ts b/back-end/libs/common/src/utils/client/index.ts index 6805d712a..e80336f70 100644 --- a/back-end/libs/common/src/utils/client/index.ts +++ b/back-end/libs/common/src/utils/client/index.ts @@ -1,15 +1,17 @@ import { ClientProxy } from '@nestjs/microservices'; import { + EXECUTE_TRANSACTION, NOTIFY_CLIENT, NOTIFY_TRANSACTION_WAITING_FOR_SIGNATURES, TRANSACTION_ACTION, + UPDATE_TRANSACTION_STATUS, + SYNC_INDICATORS, NotifyClientDto, NotifyForTransactionDto, - UPDATE_TRANSACTION_STATUS, UpdateTransactionStatusDto, - SYNC_INDICATORS, SyncIndicatorsDto, + ExecuteTransactionDto, } from '@app/common'; import { TransactionStatus } from '@entities'; @@ -20,6 +22,10 @@ export const emitUpdateTransactionStatus = (client: ClientProxy, id: number) => }); }; +export const emitExecuteTranasction = (client: ClientProxy, dto: ExecuteTransactionDto) => { + client.emit(EXECUTE_TRANSACTION, dto); +}; + /* Notifications */ export const notifyTransactionAction = (client: ClientProxy) => { client.emit(NOTIFY_CLIENT, { diff --git a/front-end/src/main/shared/constants/errorCodes.ts b/front-end/src/main/shared/constants/errorCodes.ts index c00075be7..d24dd9eac 100644 --- a/front-end/src/main/shared/constants/errorCodes.ts +++ b/front-end/src/main/shared/constants/errorCodes.ts @@ -7,7 +7,7 @@ export enum ErrorCodes { TEX = 'TEX', FST = 'FST', OTIP = 'OTIP', - OSONT = 'OSONT', + OMTIP = 'OMTIP', TNF = 'TNF', TAP = 'TAP', TAX = 'TAX', @@ -34,6 +34,7 @@ export enum ErrorCodes { NNF = 'NNF', IB = 'IB', TOS = 'TOS', + IO = 'IO', } export const ErrorMessages: { [key in ErrorCodes]: string } = { @@ -45,7 +46,7 @@ export const ErrorMessages: { [key in ErrorCodes]: string } = { [ErrorCodes.TEX]: 'Transaction already exists', [ErrorCodes.FST]: 'Failed to save transaction', [ErrorCodes.OTIP]: 'Only transactions in progress can be canceled', - [ErrorCodes.OSONT]: 'Only sign-only transactions can be archived', + [ErrorCodes.OMTIP]: 'Only manual transaction in progress can be archived', [ErrorCodes.TNF]: 'Transaction not found', [ErrorCodes.TAP]: 'Transaction already approved', [ErrorCodes.TAX]: 'Transaction already executed', @@ -73,4 +74,5 @@ export const ErrorMessages: { [key in ErrorCodes]: string } = { [ErrorCodes.NNF]: 'Notification not found', [ErrorCodes.IB]: 'Invalid body', [ErrorCodes.TOS]: 'Transaction is over the size limit and cannot be executed', + [ErrorCodes.IO]: 'Invalid operation', }; diff --git a/front-end/src/main/shared/interfaces/organization/transactions/index.ts b/front-end/src/main/shared/interfaces/organization/transactions/index.ts index aebea79c7..9630b526b 100644 --- a/front-end/src/main/shared/interfaces/organization/transactions/index.ts +++ b/front-end/src/main/shared/interfaces/organization/transactions/index.ts @@ -41,7 +41,6 @@ export const TransactionTypeName = { export enum TransactionStatus { NEW = 'NEW', // unused CANCELED = 'CANCELED', - SIGN_ONLY = 'SIGN_ONLY', REJECTED = 'REJECTED', WAITING_FOR_SIGNATURES = 'WAITING FOR SIGNATURES', WAITING_FOR_EXECUTION = 'WAITING FOR EXECUTION', @@ -62,6 +61,7 @@ export interface ITransaction { statusCode?: number; signature: string; validStart: string; + isManual: boolean; cutoffAt?: string; createdAt: string; executedAt?: string; diff --git a/front-end/src/renderer/components/ImportExternalPrivateKeyModal.vue b/front-end/src/renderer/components/ImportExternalPrivateKeyModal.vue index f385c4c3f..360ba2aa9 100644 --- a/front-end/src/renderer/components/ImportExternalPrivateKeyModal.vue +++ b/front-end/src/renderer/components/ImportExternalPrivateKeyModal.vue @@ -4,6 +4,7 @@ import { reactive, watch } from 'vue'; import { Prisma } from '@prisma/client'; import useUserStore from '@renderer/stores/storeUser'; +import useContactsStore from '@renderer/stores/storeContacts'; import { useToast } from 'vue-toast-notification'; import usePersonalPassword from '@renderer/composables/usePersonalPassword'; @@ -14,6 +15,7 @@ import { assertUserLoggedIn, getErrorMessage, isLoggedInOrganization, + safeAwait, safeDuplicateUploadKey, } from '@renderer/utils'; @@ -32,6 +34,7 @@ const emit = defineEmits(['update:show']); /* Stores */ const user = useUserStore(); +const contacts = useContactsStore(); /* Composables */ const toast = useToast(); @@ -78,6 +81,7 @@ const handleImportExternalKey = async () => { await user.storeKey(keyPair, null, personalPassword, false); await user.refetchUserState(); + await safeAwait(contacts.fetch()); emit('update:show', false); diff --git a/front-end/src/renderer/components/KeyPair/ImportEncrypted/components/DecryptKeys.vue b/front-end/src/renderer/components/KeyPair/ImportEncrypted/components/DecryptKeys.vue index a5bb36167..cfa96db9d 100644 --- a/front-end/src/renderer/components/KeyPair/ImportEncrypted/components/DecryptKeys.vue +++ b/front-end/src/renderer/components/KeyPair/ImportEncrypted/components/DecryptKeys.vue @@ -2,13 +2,14 @@ import { computed, ref } from 'vue'; import useUserStore from '@renderer/stores/storeUser'; +import useContactsStore from '@renderer/stores/storeContacts'; import { useToast } from 'vue-toast-notification'; import usePersonalPassword from '@renderer/composables/usePersonalPassword'; import { hashData } from '@renderer/services/electronUtilsService'; -import { getKeysFromSecretHash, getRecoveryPhraseHashValue } from '@renderer/utils'; +import { getKeysFromSecretHash, getRecoveryPhraseHashValue, safeAwait } from '@renderer/utils'; import DecryptKeyModal from '@renderer/components/KeyPair/ImportEncrypted/components/DecryptKeyModal.vue'; @@ -25,6 +26,7 @@ const emit = defineEmits<{ /* Stores */ const user = useUserStore(); +const contacts = useContactsStore(); /* Composables */ const toast = useToast(); @@ -101,6 +103,7 @@ async function end() { await user.refetchKeys(); user.refetchAccounts(); await user.refetchUserState(); + safeAwait(contacts.fetch()); } function reset() { diff --git a/front-end/src/renderer/components/Transaction/Create/BaseTransaction/BaseTransaction.vue b/front-end/src/renderer/components/Transaction/Create/BaseTransaction/BaseTransaction.vue index 95e34b25f..fba1ed8ed 100644 --- a/front-end/src/renderer/components/Transaction/Create/BaseTransaction/BaseTransaction.vue +++ b/front-end/src/renderer/components/Transaction/Create/BaseTransaction/BaseTransaction.vue @@ -68,7 +68,7 @@ const baseGroupHandlerRef = ref | null>(nu const name = ref(''); const description = ref(''); -const isSignOnly = ref(false); +const submitManually = ref(false); const data = reactive({ payerId: '', @@ -111,7 +111,7 @@ const handleCreate = async () => { transactionBytes: props.createTransaction({ ...data } as TransactionCommonData).toBytes(), name: name.value.trim(), description: description.value.trim(), - isSignOnly: isSignOnly.value, + submitManually: submitManually.value, }, observers.value, approvers.value, @@ -202,7 +202,7 @@ defineExpose({
diff --git a/front-end/src/renderer/components/Transaction/TransactionHeaderControls.vue b/front-end/src/renderer/components/Transaction/TransactionHeaderControls.vue index 5f51d9ec3..a420aa69c 100644 --- a/front-end/src/renderer/components/Transaction/TransactionHeaderControls.vue +++ b/front-end/src/renderer/components/Transaction/TransactionHeaderControls.vue @@ -13,7 +13,7 @@ import SaveDraftButton from '@renderer/components/SaveDraftButton.vue'; /* Props */ defineProps<{ - isSignOnly: boolean; + submitManually: boolean; createButtonLabel: string; headingText?: string; loading?: boolean; @@ -25,7 +25,7 @@ defineProps<{ /* Emits */ defineEmits<{ - (event: 'update:is-sign-only', value: boolean): void; + (event: 'update:submit-manually', value: boolean): void; (event: 'add-to-group'): void; (event: 'edit-group-item'): void; }>(); @@ -56,14 +56,14 @@ useCreateTooltips(); data-bs-toggle="tooltip" data-bs-trigger="hover" data-bs-placement="bottom" - data-bs-custom-class="wide-xl-tooltip" - data-bs-title="Transaction won't be executed, it will be only signed." + data-bs-custom-class="wide-xxl-tooltip" + data-bs-title="Transaction will have to be submitted to the network manually." >
diff --git a/front-end/src/renderer/components/Transaction/TransactionProcessor/components/BigFilePersonalRequestHandler.vue b/front-end/src/renderer/components/Transaction/TransactionProcessor/components/BigFilePersonalRequestHandler.vue index 00a51d3d8..67e112bdc 100644 --- a/front-end/src/renderer/components/Transaction/TransactionProcessor/components/BigFilePersonalRequestHandler.vue +++ b/front-end/src/renderer/components/Transaction/TransactionProcessor/components/BigFilePersonalRequestHandler.vue @@ -188,7 +188,7 @@ async function processOriginal() { transaction.setContents(content.value.slice(0, FIRST_CHUNK_SIZE_BYTES)); await startChain({ - isSignOnly: false, + submitManually: false, transactionBytes: transaction.toBytes(), transactionKey: request.value.transactionKey, name: request.value.name, @@ -202,7 +202,7 @@ async function processAppend() { const transaction = createAppendTransaction(); await startChain({ - isSignOnly: false, + submitManually: false, transactionBytes: transaction.toBytes(), transactionKey: request.value.transactionKey, name: request.value.name, diff --git a/front-end/src/renderer/components/Transaction/TransactionProcessor/components/OrganizationRequestHandler.vue b/front-end/src/renderer/components/Transaction/TransactionProcessor/components/OrganizationRequestHandler.vue index c6da06850..bf23f24a2 100644 --- a/front-end/src/renderer/components/Transaction/TransactionProcessor/components/OrganizationRequestHandler.vue +++ b/front-end/src/renderer/components/Transaction/TransactionProcessor/components/OrganizationRequestHandler.vue @@ -125,7 +125,7 @@ async function submit(publicKey: string, signature: string) { network.network, signature, user.selectedOrganization.userKeys.find(k => k.publicKey === publicKey)?.id || -1, - request.value.isSignOnly, + request.value.submitManually, ); } catch (error) { emit('transaction:submit:fail', error); diff --git a/front-end/src/renderer/components/Transaction/TransactionProcessor/index.ts b/front-end/src/renderer/components/Transaction/TransactionProcessor/index.ts index 09c52dd05..3876195b1 100644 --- a/front-end/src/renderer/components/Transaction/TransactionProcessor/index.ts +++ b/front-end/src/renderer/components/Transaction/TransactionProcessor/index.ts @@ -7,7 +7,7 @@ export interface TransactionRequest { transactionBytes: Uint8Array; name: string; description: string; - isSignOnly: boolean; + submitManually: boolean; } export interface Handler { diff --git a/front-end/src/renderer/pages/RestoreKey/RestoreKey.vue b/front-end/src/renderer/pages/RestoreKey/RestoreKey.vue index 29d861920..dca3a4fcf 100644 --- a/front-end/src/renderer/pages/RestoreKey/RestoreKey.vue +++ b/front-end/src/renderer/pages/RestoreKey/RestoreKey.vue @@ -4,6 +4,7 @@ import { PrivateKey } from '@hashgraph/sdk'; import { Prisma } from '@prisma/client'; import useUserStore from '@renderer/stores/storeUser'; +import useContactsStore from '@renderer/stores/storeContacts'; import { useRouter } from 'vue-router'; import { useToast } from 'vue-toast-notification'; @@ -30,6 +31,7 @@ import RecoveryPhraseNicknameInput from '@renderer/components/RecoveryPhrase/Rec /* Stores */ const user = useUserStore(); +const contacts = useContactsStore(); /* Composables */ const toast = useToast(); @@ -164,6 +166,7 @@ const handleSaveKey = async () => { toast.success('Key pair saved'); router.push({ name: 'settingsKeys' }); + await safeAwait(contacts.fetch()); } catch (error) { toast.error(getErrorMessage(error, 'Failed to store private key')); } finally { diff --git a/front-end/src/renderer/pages/TransactionDetails/components/TransactionDetailsHeader.vue b/front-end/src/renderer/pages/TransactionDetails/components/TransactionDetailsHeader.vue index 941e93c7a..226589585 100644 --- a/front-end/src/renderer/pages/TransactionDetails/components/TransactionDetailsHeader.vue +++ b/front-end/src/renderer/pages/TransactionDetails/components/TransactionDetailsHeader.vue @@ -21,7 +21,7 @@ import { archiveTransaction, cancelTransaction, getUserShouldApprove, - markAsSignOnlyTransaction, + executeTransaction, sendApproverChoice, uploadSignatureMap, } from '@renderer/services/organization'; @@ -53,7 +53,7 @@ type ActionButton = | 'Next' | 'Cancel' | 'Export' - | 'Mark sign-only' + | 'Submit' | 'Archive'; /* Misc */ @@ -62,7 +62,7 @@ const approve: ActionButton = 'Approve'; const sign: ActionButton = 'Sign'; const next: ActionButton = 'Next'; const cancel: ActionButton = 'Cancel'; -const markAsSignOnly: ActionButton = 'Mark sign-only'; +const execute: ActionButton = 'Submit'; const archive: ActionButton = 'Archive'; const exportName: ActionButton = 'Export'; @@ -73,7 +73,7 @@ const buttonsDataTestIds: { [key: string]: string } = { [sign]: 'button-sign-org-transaction', [next]: 'button-next-org-transaction', [cancel]: 'button-cancel-org-transaction', - [markAsSignOnly]: 'button-mark-sign-only-org-transaction', + [execute]: 'button-execute-org-transaction', [archive]: 'button-archive-org-transaction', [exportName]: 'button-export-transaction', }; @@ -132,7 +132,6 @@ const transactionIsInProgress = computed( TransactionStatus.NEW, TransactionStatus.WAITING_FOR_EXECUTION, TransactionStatus.WAITING_FOR_SIGNATURES, - TransactionStatus.SIGN_ONLY, ].includes(props.organizationTransaction.status), ); @@ -158,13 +157,18 @@ const visibleButtons = computed(() => { if (!fullyLoaded.value) return buttons; const status = props.organizationTransaction?.status; + const isManual = props.organizationTransaction?.isManual; - /* The order is important */ + /* The order is important REJECT, APPROVE, SIGN, SUBMIT, NEXT, CANCEL, ARCHIVE, EXPORT */ shouldApprove.value && buttons.push(reject, approve); canSign.value && !shouldApprove.value && buttons.push(sign); + status === TransactionStatus.WAITING_FOR_EXECUTION && + isManual && + canCancel.value && + buttons.push(execute); props.nextId && !shouldApprove.value && !canSign.value && buttons.push(next); - canCancel.value && status !== TransactionStatus.SIGN_ONLY && buttons.push(cancel, markAsSignOnly); - status === TransactionStatus.SIGN_ONLY && buttons.push(archive); + canCancel.value && buttons.push(cancel); + canCancel.value && isManual && buttons.push(archive); buttons.push(exportName); return buttons; @@ -313,7 +317,7 @@ const handleApprove = async (approved: boolean, showModal?: boolean) => { }; const handleTransactionAction = async ( - action: 'cancel' | 'archive' | 'markAsSignOnly', + action: 'cancel' | 'archive' | 'execute', showModal?: boolean, ) => { assertIsLoggedInOrganization(user.selectedOrganization); @@ -338,13 +342,13 @@ const handleTransactionAction = async ( successMessage: 'Transaction archived successfully', actionFunction: archiveTransaction, }, - markAsSignOnly: { - title: 'Mark transaction as sign-only?', - text: 'Are you sure you want to mark the transaction as sign-only? It will not be executed, only signed.', + execute: { + title: 'Submit Transaction?', + text: 'Are you sure you want to send the transaction for execution?', buttonText: 'Confirm', - loadingText: 'Updating...', - successMessage: 'Transaction marked as sign-only successfully', - actionFunction: markAsSignOnlyTransaction, + loadingText: 'Executing...', + successMessage: 'Transaction sent for execution successfully', + actionFunction: executeTransaction, }, }; @@ -373,16 +377,11 @@ const handleTransactionAction = async ( isConfirmModalLoadingState.value = false; confirmModalLoadingText.value = ''; } - - if (action === 'cancel') { - router.back(); - } }; const handleCancel = (showModal?: boolean) => handleTransactionAction('cancel', showModal); const handleArchive = (showModal?: boolean) => handleTransactionAction('archive', showModal); -const handleMarkSignOnly = (showModal?: boolean) => - handleTransactionAction('markAsSignOnly', showModal); +const handleExecute = (showModal?: boolean) => handleTransactionAction('execute', showModal); const handleNext = () => { if (!props.nextId) return; @@ -431,8 +430,8 @@ const handleAction = async (value: ActionButton) => { await handleCancel(true); } else if (value === archive) { await handleArchive(true); - } else if (value === markAsSignOnly) { - await handleMarkSignOnly(true); + } else if (value === execute) { + await handleExecute(true); } else if (value === exportName) { await handleExport(); } diff --git a/front-end/src/renderer/services/organization/transaction.ts b/front-end/src/renderer/services/organization/transaction.ts index ef1e709ee..b5266cef6 100644 --- a/front-end/src/renderer/services/organization/transaction.ts +++ b/front-end/src/renderer/services/organization/transaction.ts @@ -36,7 +36,7 @@ export const submitTransaction = async ( network: Network, signature: string, creatorKeyId: number, - isSignOnly?: boolean, + isManual?: boolean, ): Promise<{ id: number; transactionBytes: string }> => commonRequestHandler(async () => { const { data } = await axiosWithCredentials.post(`${serverUrl}/${controller}`, { @@ -46,7 +46,7 @@ export const submitTransaction = async ( mirrorNetwork: network, signature, creatorKeyId, - isSignOnly, + isManual, }); return { id: data.id, transactionBytes: data.transactionBytes }; @@ -68,15 +68,13 @@ export const archiveTransaction = async (serverUrl: string, id: number): Promise return data; }, `Failed to archive transaction with id ${id}`); -/* Mark sign-only a transaction */ -export const markAsSignOnlyTransaction = async (serverUrl: string, id: number): Promise => +/* Executes the manual transaction */ +export const executeTransaction = async (serverUrl: string, id: number): Promise => commonRequestHandler(async () => { - const { data } = await axiosWithCredentials.patch( - `${serverUrl}/${controller}/mark-sign-only/${id}`, - ); + const { data } = await axiosWithCredentials.patch(`${serverUrl}/${controller}/execute/${id}`); return data; - }, `Failed to mark transaction with id ${id} as sign-only`); + }, `Failed to execute transaction with id ${id}`); /* Decrypt, sign, upload signatures to the backend */ export const uploadSignatureMap = async (