diff --git a/packages/massa-web3/src/interfaces/EOperationStatus.ts b/packages/massa-web3/src/interfaces/EOperationStatus.ts index f75fe506..c34bab8c 100644 --- a/packages/massa-web3/src/interfaces/EOperationStatus.ts +++ b/packages/massa-web3/src/interfaces/EOperationStatus.ts @@ -5,8 +5,8 @@ export enum EOperationStatus { INCLUDED_PENDING = 0, AWAITING_INCLUSION = 1, FINAL_SUCCESS = 2, - NOT_FOUND = 4, INCONSISTENT = 3, + NOT_FOUND = 4, FINAL_ERROR = 5, SPECULATIVE_SUCCESS = 6, SPECULATIVE_ERROR = 7, diff --git a/packages/massa-web3/src/web3/SmartContractsClient.ts b/packages/massa-web3/src/web3/SmartContractsClient.ts index d1aa43bd..375cb182 100644 --- a/packages/massa-web3/src/web3/SmartContractsClient.ts +++ b/packages/massa-web3/src/web3/SmartContractsClient.ts @@ -333,35 +333,28 @@ export class SmartContractsClient public async getOperationStatus(opId: string): Promise { const operationData: Array = await this.publicApiClient.getOperations([opId]); - if (!operationData || operationData.length === 0) - return EOperationStatus.NOT_FOUND; - const opData = operationData[0]; - if (opData.is_operation_final === null && opData.op_exec_status === null) { + + if (!operationData?.length) return EOperationStatus.NOT_FOUND; + + const { is_operation_final, op_exec_status, in_pool, in_blocks } = + operationData[0]; + + if (is_operation_final === null && op_exec_status === null) return EOperationStatus.UNEXECUTED_OR_EXPIRED; - } - if (opData.in_pool) { - return EOperationStatus.AWAITING_INCLUSION; - } - if (opData.is_operation_final && opData.op_exec_status) { - return EOperationStatus.FINAL_SUCCESS; - } - // since null is a falsy value, we need to check for false explicitly - if (opData.is_operation_final && opData.op_exec_status === false) { - return EOperationStatus.FINAL_ERROR; - } - if (opData.is_operation_final === false && opData.op_exec_status) { - return EOperationStatus.SPECULATIVE_SUCCESS; - } - if ( - opData.is_operation_final === false && - opData.op_exec_status === false - ) { - return EOperationStatus.SPECULATIVE_ERROR; - } - if (opData.in_blocks.length > 0) { - return EOperationStatus.INCLUDED_PENDING; + + if (in_pool) return EOperationStatus.AWAITING_INCLUSION; + + if (is_operation_final) { + if (op_exec_status) return EOperationStatus.FINAL_SUCCESS; + // We explicitly check for false here because null means that the operation was not executed + if (op_exec_status === false) return EOperationStatus.FINAL_ERROR; + } else { + if (op_exec_status) return EOperationStatus.SPECULATIVE_SUCCESS; + if (op_exec_status === false) return EOperationStatus.SPECULATIVE_ERROR; } + if (in_blocks.length > 0) return EOperationStatus.INCLUDED_PENDING; + return EOperationStatus.INCONSISTENT; } @@ -376,16 +369,59 @@ export class SmartContractsClient public async awaitRequiredOperationStatus( opId: string, requiredStatus: EOperationStatus, + timeout?: number, + ): Promise { + return await this.awaitOperationStatusHelper( + opId, + timeout, + (currentStatus) => currentStatus === requiredStatus, + ); + } + + /** + * Get the status of a specific operation and wait until it reaches one of the required statuses. + * + * @param opId - The required operation id. + * @param requiredStatuses - An array of required statuses. + * + * @returns A promise that resolves to the status of the operation. + */ + public async awaitMultipleRequiredOperationStatus( + opId: string, + requiredStatuses: EOperationStatus[], + timeout?: number, + ): Promise { + return await this.awaitOperationStatusHelper( + opId, + timeout, + (currentStatus) => requiredStatuses.includes(currentStatus), + ); + } + + /** + * Helper method to wait for a specific condition on an operation's status. + * + * @param opId - The operation id to check. + * @param statusCheck - A callback function that defines the condition for the operation status. + * + * @returns A promise that resolves to the status of the operation. + * + * @private + */ + private async awaitOperationStatusHelper( + opId: string, + timeout = WAIT_STATUS_TIMEOUT, + statusCheck: (status: EOperationStatus) => boolean, ): Promise { const startTime = Date.now(); - while (Date.now() - startTime < WAIT_STATUS_TIMEOUT) { + while (Date.now() - startTime < timeout) { let currentStatus = EOperationStatus.NOT_FOUND; try { currentStatus = await this.getOperationStatus(opId); - if (currentStatus === requiredStatus) { + if (statusCheck(currentStatus)) { return currentStatus; } } catch (ex) { diff --git a/packages/massa-web3/test/web3/smartContractsClient.spec.ts b/packages/massa-web3/test/web3/smartContractsClient.spec.ts index 41ec0dc7..bc194879 100644 --- a/packages/massa-web3/test/web3/smartContractsClient.spec.ts +++ b/packages/massa-web3/test/web3/smartContractsClient.spec.ts @@ -522,6 +522,64 @@ describe('SmartContractsClient', () => { }); }); + describe('awaitMultipleRequiredOperationStatuses', () => { + const opId = mockOpIds[0]; + const requiredStatus = EOperationStatus.FINAL_SUCCESS; + const timeout = 1000; + let getOperationStatusMock; + + beforeEach(() => { + getOperationStatusMock = jest.spyOn( + smartContractsClient, + 'getOperationStatus', + ); + }); + + afterEach(() => { + getOperationStatusMock.mockReset(); + }); + + it('should return the expected status when all required statuses are met', async () => { + getOperationStatusMock + .mockResolvedValueOnce(EOperationStatus.NOT_FOUND) + .mockResolvedValueOnce(requiredStatus); + + const requiredStatuses = [ + EOperationStatus.FINAL_SUCCESS, + EOperationStatus.FINAL_ERROR, + ]; + + const result = + await smartContractsClient.awaitMultipleRequiredOperationStatus( + opId, + requiredStatuses, + timeout, + ); + + expect(result).toEqual(requiredStatus); + expect(smartContractsClient.getOperationStatus).toHaveBeenCalledTimes(2); + }); + + it('should throw an error when the required statuses are not met within the timeout', async () => { + getOperationStatusMock.mockResolvedValue(EOperationStatus.NOT_FOUND); // Always return NOT_FOUND + + const requiredStatuses = [ + EOperationStatus.FINAL_SUCCESS, + EOperationStatus.FINAL_ERROR, + ]; + + await expect( + smartContractsClient.awaitMultipleRequiredOperationStatus( + opId, + requiredStatuses, + timeout, + ), + ).rejects.toThrow( + `Failed to retrieve status of operation id: ${opId}: Timeout reached.`, + ); + }); + }); + describe('getContractBalance', () => { const expectedBalance: IBalance = { candidate: fromMAS(mockAddressesInfo[0].candidate_balance),