Skip to content

Commit

Permalink
Add awaitMultipleRequiredOperationStatuses method (#487)
Browse files Browse the repository at this point in the history
  • Loading branch information
Ben-Rey authored Oct 17, 2023
1 parent 3bdddb9 commit e83234d
Show file tree
Hide file tree
Showing 3 changed files with 123 additions and 29 deletions.
2 changes: 1 addition & 1 deletion packages/massa-web3/src/interfaces/EOperationStatus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
92 changes: 64 additions & 28 deletions packages/massa-web3/src/web3/SmartContractsClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -333,35 +333,28 @@ export class SmartContractsClient
public async getOperationStatus(opId: string): Promise<EOperationStatus> {
const operationData: Array<IOperationData> =
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;
}

Expand All @@ -376,16 +369,59 @@ export class SmartContractsClient
public async awaitRequiredOperationStatus(
opId: string,
requiredStatus: EOperationStatus,
timeout?: number,
): Promise<EOperationStatus> {
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<EOperationStatus> {
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

Check warning on line 409 in packages/massa-web3/src/web3/SmartContractsClient.ts

View workflow job for this annotation

GitHub Actions / build

tsdoc-undefined-tag: The TSDoc tag "@Private" is not defined in this configuration

Check warning on line 409 in packages/massa-web3/src/web3/SmartContractsClient.ts

View workflow job for this annotation

GitHub Actions / test / build

tsdoc-undefined-tag: The TSDoc tag "@Private" is not defined in this configuration
*/
private async awaitOperationStatusHelper(
opId: string,
timeout = WAIT_STATUS_TIMEOUT,
statusCheck: (status: EOperationStatus) => boolean,
): Promise<EOperationStatus> {
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) {
Expand Down
58 changes: 58 additions & 0 deletions packages/massa-web3/test/web3/smartContractsClient.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down

0 comments on commit e83234d

Please sign in to comment.